// jsg.js, (C) Walter Bislin, walter.bislins.ch, Februar 2009
//
// description and download:
// http://walter.bislins.ch/doku/jsg
//
// dependecies:
// x.js
//
// History:
// 06.11.2016 new: graphic clipping implemented: SetGraphClipping()
// 30.08.2016 update: MakeSnapshot() uses canvas as buffer instead image, is mutch mutch faster!
// 19.08.2016 new: TextBox(), SetTextRotation(), JsgTrans2D.GetTrans(), SaveTrans(), RestoreTrans()
// update: JsgTrans2D.AddTrans() accepts also a JsgTrans2D object plus a matrix 2x3
// 27.05.2016 new function SplineCurve
// 24.05.2016 some functions also accept JsgVect2 instead of x, y
// 13.03.2016 Complete Redesign
// 14.02.2009: first Version
////////////////////////////////////
// JsgColor
var JsgColor = {
RGB: function( r, g, b ) {
return [ r, g, b, 1 ];
},
RGBA: function( r, g, b, a ) {
return [ r, g, b, a ];
},
BW: function( v ) {
return [ v, v, v, 1 ];
},
Black: function() {
return [ 0, 0, 0, 1 ];
},
White: function() {
return [ 1, 1, 1, 1 ];
},
Ok: function( col ) {
return xArray(col);
},
Alpha: function( col ) {
return xDefNum( col[3], 1 );
},
SetAlpha: function( col, a ) {
col[3] = xDefNum( a, 1 );
return col;
},
SetRGBA: function( col, r, g, b, a ) {
col[0] = r; col[1] = g; col[2] = b; col[3] = a;
return col;
},
SetRGB: function( col, r, g, b ) {
col[0] = r; col[1] = g; col[2] = b; col[3] = 1;
return col;
},
SetBW: function( col, v ) {
col[0] = v; col[1] = v; col[2] = v; col[3] = 1;
return col;
},
SetBlack: function( col ) {
col[0] = 0; col[1] = 0; col[2] = 0; col[3] = 1;
return col;
},
SetWhite: function( col ) {
col[0] = 1; col[1] = 1; col[2] = 1; col[3] = 1;
return col;
},
Copy: function( src ) {
return [ src[0], src[1], src[2], this.DefNum(src[3],1) ];
},
CopyTo: function( src, dest ) {
dest[0] = src[0]; dest[1] = src[1]; dest[2] = src[2]; dest[3] = this.DefNum(src[3],1);
return dest;
},
Scale: function( col, s ) {
col[0] *= s; col[1] *= s; col[2] *= s;
return col;
},
Add: function( col, add ) {
col[0] += add[0]; col[1] += add[1]; col[2] += add[2];
return col;
},
Mult: function( col, mul ) {
col[0] *= mul[0]; col[1] *= mul[1]; col[2] *= mul[2];
return col;
},
ToString: function( col ) {
// element of col: number; number usually 0..1 but may be out of this range too
function normCol( cx ) {
cx = Math.round( cx * 255 );
return (cx > 255) ? 255 : ((cx < 0) ? 0 : cx);
}
function toHex( cx ) {
cx = normCol(cx);
var hex = cx.toString(16);
return cx < 16 ? "0" + hex : hex;
}
var a = normCol(this.DefNum(col[3],1));
if (a == 255) {
return '#' + toHex(col[0]) + toHex(col[1]) + toHex(col[2]);
} else {
return 'rgba('
+ normCol(col[0]).toFixed(0) + ','
+ normCol(col[1]).toFixed(0) + ','
+ normCol(col[2]).toFixed(0) + ','
+ (a/255).toFixed(3) + ')';
}
},
HSV: function( h, s, v, a ) {
// h, s, v, a = number(0..1)
// h = Hue = color: 0 = red; 1/6 = yellow; 2/6 = green; 3/6 = cyan; 4/6 = blue; 5/6 = magenta
// s = Saturation: 0 = gray; 1 = full color
// v = Luminosity: 0 = black; 1 = full color
// returns JsgColor = [ r, g, b, a ]
var Num = this.DefNum, Limit = this.Limit01;
h = Limit( Num( h, 1 ) );
s = Limit( Num( s, 1 ) );
v = Limit( Num( v, 1 ) );
a = Limit( Num( a, 1 ) );
var r, g, b, hi;
h *= 6;
hi = Math.floor(h) % 6;
h = h % 1;
switch(hi) {
case 0:
r = 1; g = h; b = 0;
break;
case 1:
r = 1 - h; g = 1; b = 0;
break;
case 2:
r = 0; g = 1; b = h;
break;
case 3:
r = 0; g = 1 - h; b = 1;
break;
case 4:
r = h; g = 0; b = 1;
break;
default:
r = 1; g = 0; b = 1 - h;
}
r = v * (1 - (1 - r) * s);
g = v * (1 - (1 - g) * s);
b = v * (1 - (1 - b) * s);
return [ r, g, b, a ];
},
HL: function( h, l, a ) {
// l -> s, v: l=0 -> v=0, s=1; l=0.5 -> v=1, s=1; l=1 -> v=1, s=0
// h = Hue = color: 0 = red; 1/6 = yellow; 2/6 = green; 3/6 = cyan; 4/6 = blue; 5/6 = magenta
// s = Saturation: 0 = gray; 1 = full color
// v = Luminosity: 0 = black; 1 = full color
l = this.Limit01( this.DefNum( l, 0.5 ) );
var s, v;
if (l < 0.5) {
s = 1; v = 2 * l;
} else {
v = 1; s = 1 - 2 * (l - 0.5);
}
return this.HSV( h, s, v, a );
},
DefNum: function( x, def ) {
return (typeof(x) === 'number') ? x : def;
},
Limit01: function(x) {
return (x < 0) ? 0 : ((x > 1) ? 1 : x);
}
};
////////////////////////////////////
// JsgVect2
var JsgVect2 = {
New: function( x, y ) {
return [ x, y ];
},
Set: function( v, x, y ) {
v[0] = x;
v[1] = y;
return v;
},
Null: function() {
return [ 0, 0 ];
},
Ok: function( v ) {
return xArray(v); // && v.length >= 2
},
Scale: function( v, s ) {
return [ s * v[0], s * v[1] ];
},
Add: function( v1, v2 ) {
return [ v1[0] + v2[0], v1[1] + v2[1] ];
},
Sub: function( v1, v2 ) {
return [ v1[0] - v2[0], v1[1] - v2[1] ];
},
Length: function( v ) {
var x = v[0], y = v[1];
return Math.sqrt( x * x + y * y );
},
Length2: function( x, y ) {
return x * x + y * y;
},
Norm: function( v ) {
var s = this.Length(v);
if (s == 0) s = 1;
return [ v[0] / s, v[1] / s ];
},
ScalarProd: function( v1, v2 ) {
return v1[0] * v2[0] + v1[1] * v2[1];
},
VectProd: function( u, v ) {
return u[0] * v[1] - u[1] * v[0];
},
Rotate: function( v, ang ) {
var c = Math.cos( ang );
var s = Math.sin( ang );
return [ c * v[0] - s * v[1], s * v[0] + c * v[1] ];
},
Angle: function( u, v, norm ) {
// require u and v are of length 1 -> Norm()
// returns angle between vectors from u to v.
// positiv angle is counter clockwise
// -pi < angle <= pi
norm = xDefBool( norm, false );
if (norm) {
u = this.Norm( u );
v = this.Norm( v );
}
var sign = Math.asin( this.VectProd( u, v ) ) < 0 ? -1 : 1;
return sign * Math.acos( this.ScalarProd( u, v ) );
},
};
//////////////////////////////////
// JsgMat2
JsgMat2 = {
Zero: function() {
return [ [0, 0, 0], [0, 0, 0] ];
},
Unit: function() {
return [ [1, 0, 0], [0, 1, 0] ];
},
Ok: function( mat ) {
return xArray(mat);
},
RotatingToXY: function( x, y ) {
// returns a rotation matrix that rotates by the angle between (x,y) and the X-Axes.
var vl = Math.sqrt( x * x + y * y );
if (vl == 0) {
x = 1; y = 0;
} else {
x /= vl; y /= vl;
}
return [ [ x, -y, 0 ], [ y, x, 0 ] ];
},
Transformation: function( sx, sy, rot, x, y ) {
var cosRot = Math.cos(rot);
var sinRot = Math.sin(rot);
return [ [ cosRot*sx, -sinRot*sy, x ], [ sinRot*sx, cosRot*sy, y ] ];
},
Moving: function( x, y ) {
return [ [1, 0, x], [0, 1, y] ];
},
Scaling: function( sx, sy ) {
return [ [sx, 0, 0], [0, sy, 0] ];
},
Rotating: function( ang ) {
var c = Math.cos(ang);
var s = Math.sin(ang);
return [ [c, -s, 0], [s, c, 0] ];
},
Mult: function( matA, matB ) {
r = this.Null();
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
r[i][j] = matA[i][0] * matB[0][j] + matA[i][1] * matB[1][j];
}
r[i][2] = matA[i][0] * matB[0][2] + matA[i][1] * matB[1][2] + matA[i][2];
}
return r;
},
Move: function( mat, x, y ) {
return this.Mult( mat, this.Moving( x, y ) );
},
Rotate: function( mat, ang ) {
return this.Mult( mat, this.Rotating( ang ) );
},
Scale: function( mat, sx, sy ) {
return this.Mult( mat, this.Scaling( sx, sy ) );
},
Trans: function( mat, v ) {
// v: JsgVect2
var x = v[0] * mat[0][0] + v[1] * mat[0][1] + mat[0][2];
v[1] = v[0] * mat[1][0] + v[1] * mat[1][1] + mat[1][2];
v[0] = x;
},
TransPolyXY: function( mat, polyX, polyY, size ) {
var l = xDefNum( size, polyX.length );
for (var i = 0; i < l; i++) {
var x = polyX[i] * mat[0][0] + polyY[i] * mat[0][1] + mat[0][2];
polyY[i] = polyX[i] * mat[1][0] + polyY[i] * mat[1][1] + mat[1][2];
polyX[i] = x;
}
},
};
////////////////////////////////////
// JsgRect
function JsgRect( x, y, w, h ) {
this.Set( x, y, w, h );
}
JsgRect.prototype.SetPos = function( x, y ) {
this.x = x;
this.y = y;
}
JsgRect.prototype.SetSize = function( w, h ) {
this.w = w;
this.h = h;
}
JsgRect.prototype.Set = function( x, y, w, h ) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
JsgRect.Ok = function( obj ) {
return xDef(obj) && xDef(obj.x) && xDef(obj.w);
};
////////////////////////////////////
// JsgGradient
function JsgGradient( gradType, canvasGrad, gradDef ) {
this.Type = gradType; // 'linear' or 'radial'
this.CanvasGradient = canvasGrad;
this.GradientDef = gradDef;
}
JsgGradient.Ok = function( obj ) {
return xObj(obj) && xDef(obj.CanvasGradient);
};
////////////////////////////////////
// JsgPolygon, for 2D and 3D
function JsgPolygon( Type3D, owner ) {
// if Type3D is true then Z-Array is used else it is null and ignored
// Note: Arrays X, Y and Z are usually larger then Size. Use Copy function to optain arrays of size aSize.
this._Owner = xDefStr( owner, '' );
this.Init( Type3D );
}
JsgPolygon.prototype.Is3D = function() {
return this.Z != null;
}
JsgPolygon.prototype.Init = function( Type3D ) {
// if Type3D is true then Z-Array is used else it is null and ignored (2D polygon)
this.X = [];
this.Y = [];
this.Z = Type3D ? [] : null;
this.Size = 0;
return this;
}
JsgPolygon.Ok = function( obj ) {
return xObj(obj) && xArray(obj.X);
}
JsgPolygon.prototype.Reset = function() {
// keep and reuse arrays!
this.Size = 0;
return this;
}
JsgPolygon.prototype.IsEmpty = function() {
return this.Size == 0;
}
JsgPolygon.prototype.GetFirstPoint3D = function( p ) {
if (this.Size < 0) return false;
p[0] = this.X[0]; p[1] = this.Y[0]; p[2] = this.Z[0];
return true;
}
JsgPolygon.prototype.GetLastPoint3D = function( p ) {
var last = this.Size-1;
if (last < 0) return false;
p[0] = this.X[last]; p[1] = this.Y[last]; p[2] = this.Z[last];
return true;
}
JsgPolygon.prototype.AddPoint = function( x, y, z ) {
// z is ignored if poly is only 2D
// automatic enlarges arrays if array.length <= Size
this.X[this.Size] = x;
this.Y[this.Size] = y;
if (this.Z) this.Z[this.Size] = z;
this.Size++
return this;
}
JsgPolygon.prototype.AddPoint3D = function( p ) {
// p: array[3] of number (e.g. JsgVect3)
// automatic enlarges arrays if array.length <= Size
this.X[this.Size] = p[0];
this.Y[this.Size] = p[1];
this.Z[this.Size] = p[2];
this.Size++
return this;
}
JsgPolygon.prototype.AddPoly = function( poly, offset ) {
offset = xDefNum( offset, 0 );
var xs = poly.X;
var ys = poly.Y;
var zs = poly.Z;
var xd = this.X;
var yd = this.Y;
var zd = this.Z;
var dSize = this.Size;
var size = poly.Size;
for (var i = offset; i < size; i++) {
xd[dSize] = xs[i];
yd[dSize] = ys[i];
if (zd) zd[dSize] = zs[i];
dSize++;
}
this.Size = dSize;
return this;
}
JsgPolygon.prototype.RemoveLastPoint = function() {
this.Size--;
if (this.Size < 0) this.Size = 0;
return this;
}
JsgPolygon.prototype.Close = function() {
// adds first point of poly to the end if they are not equal
// returns true if points weren't equal and point is added
// use RemoveLastPoint to remove added point if necessary
if (this.Size < 2) return false;
if (this.IsSamePoint( 0, this, this.Size-1 )) return false;
if (this.Z) {
this.AddPoint( this.X[0], this.Y[0], this.Z[0] );
} else {
this.AddPoint( this.X[0], this.Y[0] );
}
return true;
}
JsgPolygon.prototype.IsSamePoint = function( i, poly, j ) {
// returns true if point i of this is the same as point j of poly
if (this.Z) {
return this.X[i] == poly.X[j] && this.Y[i] == poly.Y[j] && this.Z[i] == poly.Z[j];
} else {
return this.X[i] == poly.X[j] && this.Y[i] == poly.Y[j];
}
}
JsgPolygon.prototype.Copy = function( to, useNewArrays ) {
// to: JsgPolygon (optional)
// returns a copy in 'to' or in a new JsgPolygon object.
// if useNewArray is true, returned arrays are not reused arrays and have.length = this.Size, never larger!
to = to || new JsgPolygon( this.Is3D() );
if (useNewArrays) to.Init( this.Is3D );
var toX = to.X
var toY = to.Y;
var toZ = to.Z;
var fromX = this.X;
var fromY = this.Y;
var fromZ = this.Z;
var len = this.Size;
for (var i = 0; i < len; i++) {
toX[i] = fromX[i];
toY[i] = fromY[i];
if (fromZ) toZ[i] = fromZ[i];
}
to.Size = len;
return to;
}
JsgPolygon.WorkArray = [];
JsgPolygon.InvertArrays = function( xArr, yArr, zArr, size ) {
// inverses the order of elements in arrays
function InvertArray( a ) {
var last = Math.floor(size/2) - 1;
for (var i = 0, j = size-1; i <= last; i++, j--) {
var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
}
}
size = xDefNum( size, xArr.length );
if (size < 2) return;
InvertArray( xArr );
InvertArray( yArr );
if (zArr) InvertArray( zArr );
}
JsgPolygon.prototype.Invert = function( ) {
JsgPolygon.InvertArrays( this.X, this.Y, this.Z, this.Size );
}
JsgPolygon.RollArrays = function( xArr, yArr, zArr, n, size ) {
// rolls elements of array a down n steps or up, if n is negative
function RollArray( a ) {
var src = Math.abs(n) % size;
if (n < 0) src = size - src;
var newArr = JsgPolygon.WorkArray;
for (var dest = 0; dest < size; dest++) {
newArr[dest] = a[src++];
if (src >= size) src = 0;
}
for (var i = 0; i < size; i++) a[i] = newArr[i];
}
size = xDefNum( size, xArr.length );
if (size < 2) return;
RollArray( xArr );
RollArray( yArr );
if (zArr) RollArray( zArr );
}
JsgPolygon.prototype.Roll = function( n ) {
// rolls points of polygon down n steps or up, if n is negative
JsgPolygon.RollArrays( this.X, this.Y, this.Z, n, this.Size );
}
////////////////////////////////////
// JsgPolygonList
function JsgPolygonList( Type3D, owner ) {
// set Type3D true if poly list contains 3D polygons
// Note: PolyList is usually larger then Size.
this._Owner = xDefStr( owner, '' );
this.PolyList = [];
this.Size = 0;
this.CurrPoly = null;
this.Type3D = xDefBool( Type3D, false );
}
JsgPolygonList.Ok = function( obj ) {
return xObj(obj) && xArray(obj.PolyList);
}
JsgPolygonList.prototype.Is3D = function() {
return this.Type3D;
}
JsgPolygonList.prototype.IsEmpty = function() {
return this.Size == 0 || this.PolyList[0].IsEmpty();
}
JsgPolygonList.prototype.Reset = function() {
this.Size = 0;
this.CurrPoly = null;
return this;
}
JsgPolygonList.prototype.NewPoly = function() {
if (this.PolyList.length > this.Size) {
this.PolyList[this.Size].Reset();
} else {
this.PolyList[this.Size] = new JsgPolygon( this.Type3D );
}
this.CurrPoly = this.PolyList[this.Size];
this.Size++;
return this;
}
JsgPolygonList.prototype.GetLastPoly = function() {
// returns last JsgPolygon in PolyList
// requires Size > 0
return this.PolyList[this.Size-1];
}
JsgPolygonList.prototype.GetFirstPoly = function() {
// returns first JsgPolygon in PolyList
// requires Size > 0
return this.PolyList[0];
}
JsgPolygonList.prototype.GetFirstPoint3D = function( p ) {
if (this.Size == 0) return false;
return this.PolyList[this.Size-1].GetFirstPoint3D( p );
}
JsgPolygonList.prototype.GetLastPoint3D = function( p ) {
if (this.Size == 0) return false;
return this.PolyList[this.Size-1].GetLastPoint3D( p );
}
JsgPolygonList.prototype.AddPoint = function( x, y, z ) {
// z on 2D polygons is ignored
// require this.Size > 0
this.CurrPoly.AddPoint( x, y, z );
return this;
}
JsgPolygonList.prototype.AddPoint3D = function( p ) {
// z on 2D polygons is ignored
// require this.Size > 0
this.CurrPoly.AddPoint3D( p );
return this;
}
JsgPolygonList.prototype.AddPoly = function( polys, appendMode ) {
// polys: JsgPolygon or JsgPolygonList
// appendMode = 0: (new) add polys as all new polys
// appendMode = 1: (extend) append if this and polys have common point else (new)
// appendMode = 2: (append) append polys to this
// appendMode != 0 merge common points
appendMode = xDefNum( appendMode, 0 );
if (!JsgPolygonList.Ok(polys)) return this.AddSinglePoly( polys, appendMode );
var n = polys.Size;
for (var i = 0; i < n; i++) {
this.AddSinglePoly( polys.PolyList[i], appendMode );
}
return this;
}
JsgPolygonList.prototype.AddSinglePoly = function( poly, appendMode ) {
// private function
if (appendMode == 0) {
this.NewPoly();
this.CurrPoly.AddPoly( poly, 0 );
} else {
var offset = 0;
if (this.Size == 0) {
this.NewPoly();
} else {
var currPoly = this.CurrPoly;
if (poly.Size > 0 && poly.IsSamePoint( 0, currPoly, currPoly.Size-1 )) {
offset = 1;
} else if (appendMode == 1) {
this.NewPoly();
}
}
this.CurrPoly.AddPoly( poly, offset );
}
return this;
}
JsgPolygonList.prototype.RemoveLastPoint = function() {
// requires last poly in PolyList must have Size > 0
this.PolyList[this.Size-1].RemoveLastPoint();
}
JsgPolygonList.prototype.Close = function() {
// adds first point of first poly to the end of last poly if they are not equal
// returns true if points weren't equal and point is added
// use RemoveLastPoint to remove added point if necessary
if (this.Size == 0) return false;
var firstPoly = this.PolyList[0];
var lastPoly = this.PolyList[this.Size-1];
if (firstPoly.Size < 1 || lastPoly.Size < 1) return false;
if (firstPoly.IsSamePoint( 0, lastPoly, lastPoly.Size-1 )) return false;
if (this.Type3D) {
lastPoly.AddPoint( firstPoly.X[0], firstPoly.Y[0], firstPoly.Z[0] );
} else {
lastPoly.AddPoint( firstPoly.X[0], firstPoly.Y[0] );
}
return true;
}
////////////////////////////////////
// JsgSnapshot
function JsgSnapshot( sx, sy, sw, sh, srcCanvas ) {
this.x = sx;
this.y = sy;
this.w = sw;
this.h = sh;
this.ImageData = null;
var buffer = xCreateElement('canvas');
if (buffer) {
buffer.width = sw;
buffer.height = sh;
buffer.getContext('2d').drawImage( srcCanvas, sx, sy, sw, sh, 0, 0, sw, sh );
this.ImageData = buffer;
}
}
////////////////////////////////////
// JsgTrans
function JsgTrans( aTransName ) {
this.Name = aTransName;
this.x = 0;
this.y = 0;
this.x1 = 0;
this.y1 = 0;
this.x2 = 0;
this.y2 = 0;
this.Reset();
}
JsgTrans.prototype.Reset = function() {
this.Xmin = 0;
this.Ymin = 0;
this.Width = 1;
this.Height = 1;
this.ScaleX = 1;
this.ScaleY = 1;
this.OffsetX = 0;
this.OffsetY = 0;
}
JsgTrans.prototype.TransX = function( x ) {
return x * this.ScaleX + this.OffsetX;
}
JsgTrans.prototype.TransY = function( y ) {
return y * this.ScaleY + this.OffsetY;
}
JsgTrans.prototype.TransXY = function( x, y ) {
this.x = x * this.ScaleX + this.OffsetX;
this.y = y * this.ScaleY + this.OffsetY;
}
JsgTrans.prototype.ObjTransXY = function( otr, x, y ) {
if (otr) {
otr.TransXY( x, y );
this.x = otr.x * this.ScaleX + this.OffsetX;
this.y = otr.y * this.ScaleY + this.OffsetY;
} else {
this.x = x * this.ScaleX + this.OffsetX;
this.y = y * this.ScaleY + this.OffsetY;
}
}
JsgTrans.prototype.ObjTransXY2 = function( otr, x1, y1, x2, y2 ) {
if (otr) {
otr.TransXY2( x1, y1, x2, y2 );
this.x1 = otr.x1 * this.ScaleX + this.OffsetX;
this.y1 = otr.y1 * this.ScaleY + this.OffsetY;
this.x2 = otr.x2 * this.ScaleX + this.OffsetX;
this.y2 = otr.y2 * this.ScaleY + this.OffsetY;
} else {
this.x1 = x1 * this.ScaleX + this.OffsetX;
this.y1 = y1 * this.ScaleY + this.OffsetY;
this.x2 = x2 * this.ScaleX + this.OffsetX;
this.y2 = y2 * this.ScaleY + this.OffsetY;
}
}
JsgTrans.prototype.InvTransX = function( x ) {
return (x - this.OffsetX) / this.ScaleX;
}
JsgTrans.prototype.InvTransY = function( y ) {
return (y - this.OffsetY) / this.ScaleY;
}
////////////////////////////////////
// JsgTrans2D
function JsgTrans2D( aTrans2D ) {
this.x = 0;
this.y = 0;
this.x1 = 0;
this.y1 = 0;
this.x2 = 0;
this.y2 = 0;
if (xDef(aTrans2D)) {
this.CopyFrom( aTrans2D );
} else {
this.Reset();
}
}
JsgTrans2D.prototype.Reset = function() {
this.a00 = 1;
this.a01 = 0;
this.a02 = 0;
this.a10 = 0;
this.a11 = 1;
this.a12 = 0;
this.IsMoveOnly = true;
this.IsUnitTrans = true;
this.Enabled = true;
}
JsgTrans2D.prototype.Enable = function( aFlag ) {
var old = this.Enabled;
this.Enabled = aFlag;
return old;
}
JsgTrans2D.prototype.CheckTransType = function() {
this.IsMoveOnly = (this.a00 == 1 && this.a01 == 0 && this.a10 == 0 && this.a11 == 1);
this.IsUnitTrans = (this.IsMoveOnly && this.a02 == 0 && this.a12 == 0);
}
JsgTrans2D.prototype.SetTrans = function( mat ) {
this.a00 = mat[0][0];
this.a01 = mat[0][1];
this.a02 = mat[0][2];
this.a10 = mat[1][0];
this.a11 = mat[1][1];
this.a12 = mat[1][2];
this.CheckTransType();
}
JsgTrans2D.prototype.GetTrans = function() {
// returns copy of internal trans array[2x3]; can be reused with SetTrans()
return [ [ this.a00, this.a01, this.a02 ], [ this.a10, this.a11, this.a12 ] ];
}
JsgTrans2D.prototype.Copy = function() {
return new JsgTrans2D( this );
}
JsgTrans2D.prototype.CopyFrom = function( aTrans2D ) {
this.a00 = aTrans2D.a00;
this.a01 = aTrans2D.a01;
this.a02 = aTrans2D.a02;
this.a10 = aTrans2D.a10;
this.a11 = aTrans2D.a11;
this.a12 = aTrans2D.a12;
this.IsMoveOnly = aTrans2D.IsMoveOnly;
this.IsUnitTrans = aTrans2D.IsUnitTrans;
this.Enabled = aTrans2D.Enabled;
}
JsgTrans2D.prototype.AddTrans = function( mat ) {
// this.a = mat * this.a
// mat: array[2x3] or JsTrans2D
var c00, c01, c02, c10, c11, c12
if (xArray(mat)) {
c00 = mat[0][0] * this.a00 + mat[0][1] * this.a10;
c01 = mat[0][0] * this.a01 + mat[0][1] * this.a11;
c02 = mat[0][0] * this.a02 + mat[0][1] * this.a12 + mat[0][2];
c10 = mat[1][0] * this.a00 + mat[1][1] * this.a10;
c11 = mat[1][0] * this.a01 + mat[1][1] * this.a11;
c12 = mat[1][0] * this.a02 + mat[1][1] * this.a12 + mat[1][2];
} else {
c00 = mat.a00 * this.a00 + mat.a01 * this.a10;
c01 = mat.a00 * this.a01 + mat.a01 * this.a11;
c02 = mat.a00 * this.a02 + mat.a01 * this.a12 + mat.a02;
c10 = mat.a10 * this.a00 + mat.a11 * this.a10;
c11 = mat.a10 * this.a01 + mat.a11 * this.a11;
c12 = mat.a10 * this.a02 + mat.a11 * this.a12 + mat.a12;
}
this.a00 = c00;
this.a01 = c01;
this.a02 = c02;
this.a10 = c10;
this.a11 = c11;
this.a12 = c12;
this.CheckTransType();
}
JsgTrans2D.prototype.Move = function( x, y ) {
this.a02 += x;
this.a12 += y;
this.CheckTransType();
}
JsgTrans2D.prototype.Scale = function( sx, sy ) {
this.a00 *= sx;
this.a01 *= sx;
this.a02 *= sx;
this.a10 *= sy;
this.a11 *= sy;
this.a12 *= sy;
this.CheckTransType();
}
JsgTrans2D.prototype.Rotate = function( ang ) {
var cosa = Math.cos(ang);
var sina = Math.sin(ang);
var c = [ [ cosa, -sina, 0 ], [ sina, cosa, 0 ] ];
this.AddTrans( c );
}
JsgTrans2D.prototype.TransX = function( x, y ) {
if (this.IsUnitTrans || !this.Enabled) return x;
if (this.IsMoveOnly) return x + this.a02;
return this.a00 * x + this.a01 * y + this.a02;
}
JsgTrans2D.prototype.TransY = function( x, y ) {
if (this.IsUnitTrans || !this.Enabled) return y;
if (this.IsMoveOnly) return y + this.a12;
return this.a10 * x + this.a11 * y + this.a12;
}
JsgTrans2D.prototype.TransXY = function( x, y ) {
this.x = x;
this.y = y;
if (this.IsUnitTrans || !this.Enabled) return;
if (this.IsMoveOnly) {
this.x += this.a02;
this.y += this.a12;
return;
}
this.x = this.a00 * x + this.a01 * y + this.a02;
this.y = this.a10 * x + this.a11 * y + this.a12;
}
JsgTrans2D.prototype.TransXY2 = function( x1, y1, x2, y2 ) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
if (this.IsUnitTrans || !this.Enabled) return;
if (this.IsMoveOnly) {
this.x1 += this.a02;
this.y1 += this.a12;
this.x2 += this.a02;
this.y2 += this.a12;
return;
}
this.x1 = this.a00 * x1 + this.a01 * y1 + this.a02;
this.y1 = this.a10 * x1 + this.a11 * y1 + this.a12;
this.x2 = this.a00 * x2 + this.a01 * y2 + this.a02;
this.y2 = this.a10 * x2 + this.a11 * y2 + this.a12;
}
JsgTrans2D.prototype.MaxScaling = function() {
var abs = Math.abs;
var t1 = abs( this.a00 ) + abs( this.a01 );
var t2 = abs( this.a10 ) + abs( this.a11 );
return Math.sqrt( (t1 * t1 + t2 * t2) / 2 );
}
////////////////////////////////////
// JsgAttrs
function JsgAttrs( aGraph ) {
this.ScaleRef = aGraph.ScaleRef;
this.LimitScalePix = aGraph.LimitScalePix;
this.AutoScalePix = aGraph.AutoScalePix;
this.ScalePixInt = aGraph.ScalePixInt;
this.ObjTrans = aGraph.ObjTrans.Copy();
this.Trans = aGraph.Trans;
this.AngleMeasure = aGraph.AngleMeasure;
this.Alpha = aGraph.Alpha;
this.LineJoin = aGraph.LineJoin;
this.LineCap = aGraph.LineCap;
this.Color = aGraph.Color;
this.BgColor = aGraph.BgColor;
this.BgGradient = aGraph.BgGradient;
this.LineWidth = aGraph.LineWidth;
this.MarkerSymbol = aGraph.MarkerSymbol;
this.MarkerSize = aGraph.MarkerSize;
this.TextRendering = aGraph.TextRendering;
this.TextClass = aGraph.TextClass;
this.TextFont = aGraph.TextFont;
this.TextSize = aGraph.TextSize;
this.TextRitation = aGraph.TextRotation;
this.TextColor = aGraph.TextColor;
this.FontStyle = aGraph.FontStyle;
this.FontWeight = aGraph.FontWeight;
this.TextHAlign = aGraph.TextHAlign;
this.TextVAlign = aGraph.TextVAlign;
this.TextHPad = aGraph.TextHPad;
this.TextVPad = aGraph.TextVPad;
this.LineHeight = aGraph.LineHeight;
this.CurvePrecision = aGraph.CurvePrecision;
}
////////////////////////////////////
// JsGraph main class
////////////////////////////////////
function NewGraph2D( aParams ) {
return new JsGraph( aParams );
}
function JsGraph( aParams ) {
// aParams =
// Id: String; Default 'JsGraph<n>', where <n> is a counter number
// Width: Number(>0) or string; default '100%'; numbers are in pixel
// Height: Number(>0) or string; default '75%'
// DrawFunc: function(JsGraph); optional; default null
// BorderWidth: number(>=0); default 1;
// BorderColor: string; default see class JsGraph { border-color }
// TextRendering: string; 'canvas' (default), 'html'
// GraphClass: string; default 'JsGraph'
// GraphFormat: string; default ''; additional CSS classes
// ScaleRef: Integer(>0); Default 800; Referenzbreite des Containers in Pixel, siehe SetScaleRef()
// AutoScalePix: Boolean; default false; true -> Skaliert Pixel-Argumente entsprechend ScaleRef -> ScalePix()
// LimitScalePix: Boolean; default true; true -> Pixel-Scaling ends when CanvasWidth is greater than ScaleRef
// ScalePixInt: Boolean; Default false; -> true automatisches ScalePix() liefert immer ganze Zahlen
// MinLineWidth: Number(>0); Default 0.1; Minimale Strickdicke bei automatischem ScalePix()
// MinTextSize: Number(>0); Default 1; Minimale Textgrösse bei automatischem ScalePix()
// MinMarkerSize: number(>0); Default 1; Minimale Grösser der Markersymbole bei automatischem ScalePix()
// DefaultAttrs: object; default standard; see SetAttrs()
// OnClick: function(xEvent,JsGraph)
// EventHandlers: [ { Event: String, Func: function(xEvent,JsGraph), Capture: Boolean }, .... ]
// }
aParams = xDefObj( aParams, {} );
this.HighResolution = xDefBool( aParams.HighResolution, true );
this.HighResSet = false;
this.DevicePixelRatio = 1;
this.CanvasPixelRatio = 1;
this.PixelRatio = 1;
this.LastPixelRatio = 0; // 0 = undefined
this.InitInternals();
this.MakeMarkers();
this.CreateCanvas( aParams );
// init some properties from aParams
if (xNum (aParams.ScaleRef)) this.ScaleRef = aParams.ScaleRef;
if (xBool(aParams.AutoScalePix)) this.AutoScalePix = aParams.AutoScalePix;
if (xBool(aParams.LimitScalePix)) this.LimitScalePix = aParams.LimitScalePix;
if (xBool(aParams.ScalePixInt)) this.ScalePixInt = aParams.ScalePixInt;
if (xNum (aParams.MinLineWidth)) this.MinLineWidth = this.MinSize( aParams.MinLineWidth, 0 );
if (xNum (aParams.MinTextSize)) this.MinTextSize = this.MinSize( aParams.MinTextSize, 0 );
if (xNum (aParams.MinMarkerSize)) this.MinMarkerSize = this.MinSize( aParams.MinMarkerSize, 0 );
if (xAny (aParams.DefaultAttrs)) this.SetAttrs( aParams.DefaultAttrs );
if (xStr (aParams.TextRendering)) this.SetTextRendering( aParams.TextRendering );
this.DeferedDrawTime = xDefNum( aParams.DeferedDrawTime, 50 );
this.AutoReset = xDefBool( aParams.AutoReset, true );
this.AutoClear = xDefBool( aParams.AutoClear, true );
this.ClientResetFunc = null; // is called in Draw if AutoReset is true
// event handlers are saved in this.EventHandlers list and installed later onDomReady event
var me = this;
this.OnResizeFunc = function CB_OnTimeout_CheckWindowResize() { me.CheckResizeRegularly(); };
this.OnDrawFunc = function CB_OnTimeout_Draw() { me.Draw(); };
this.OnDeferedDrawFunc = function CB_OnTimeout_DeferedDraw() { me.DeferedDraw(); };
if (xFunc(aParams.OnClick)) {
this.EventHandlers.push( { Event: 'click', Func: aParams.OnClick, Capture: false } );
}
if (xArray(aParams.EventHandlers)) {
var handlers = aParams.EventHandlers
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (xStr(handler.Event) && xFunc(handler.Func)) this.EventHandlers.push( handler );
}
}
// setup an OnDomReady function, which installs all event handlers in this.EventHandlers for this.Canvas
if (this.EventHandlers.length > 0) {
xOnDomReady(
function CB_InstallEventHandlers() {
var nHandlers = me.EventHandlers.length;
for (var i = 0; i < nHandlers; i++) {
var handler = me.EventHandlers[i];
me.AddEventHandler( handler.Event, handler.Func, handler.Capture );
}
me.EventHandlers = [];
}
);
}
this.SaveAttrs();
this.SaveDefaultAttrs();
this.SetDriverAttrs();
this.InitResizeCheck();
this.CheckResizeRegularly();
this.DrawPending++;
if (xFunc(aParams.DrawFunc)) this.SetDrawFunc( aParams.DrawFunc );
if (xFunc(aParams.DeferedDrawFunc)) this.SetDeferedDrawFunc( aParams.DeferedDrawFunc );
if (xFunc(aParams.BeforeResetFunc)) this.SetBeforeResetFunc( aParams.BeforeResetFunc );
}
JsGraph.prototype.InitInternals = function() {
// private function
this.LastX = 0.0;
this.LastY = 0.0;
this.BorderWidth = 0;
this.CanvasWidth = 0;
this.CanvasHeight = 0;
this.CanvasRatioHW = 0.0; // = 0 -> fixed sized canvas; > 0 -> resizable canvas
this.VpXmin = 0;
this.VpYmin = 0;
this.VpWidth = 0;
this.VpHeight = 0;
this.VpInnerWidth = 0;
this.VpInnerHeight = 0;
this.WinXmin = 0.0;
this.WinXmax = -1;
this.WinYmin = 0.0;
this.WinYmax = -1;
this.WinWidth = -1;
this.WinHeight = -1;
this.ObjTrans = new JsgTrans2D();
this.ObjTransStack = [];
this.ContextScale = 1;
this.Trans = 'window';
this.AngleMeasure = 'deg'; // 'deg' or 'rad'
this.CanvasTrans = new JsgTrans( 'canvas' );
this.VpTrans = new JsgTrans( 'viewport' );
this.WinTrans = new JsgTrans( 'window' );
this.CurrTrans = this.WinTrans;
this.TransByName = {
canvas: this.CanvasTrans,
viewport: this.VpTrans,
window: this.WinTrans
};
this.GraphClipEnabled = false;
this.GraphClipExtend = 1; // times CanvasWidth is range of outer clip
this.GraphClipMargin = 1; // adds to inner clip range
this.GraphClipInnerXmin = 0; // computed in Clip() and SetClipRect()/SetClipplin()
this.GraphClipInnerXmax = 0;
this.GraphClipInnerYmin = 0;
this.GraphClipInnerYmax = 0;
this.GraphClipOuterXmin = 0; // computet in UpdateGraphClipOuterRange()
this.GraphClipOuterXmax = 0;
this.GraphClipOuterYmin = 0;
this.GraphClipOuterYmax = 0;
this.DrawFunc = null; // function(this){}
this.DeferedDrawFunc = null; // function(this){}
this.BeforeResetFunc = null;
this.Snapshots = []; // array of JsgSnapshot
this.Poly = new JsgPolygon( false, 'JsGraph.Poly' );
this.WorkPoly = new JsgPolygon( false, 'JsGraph.WorkPoly' );
this.WorkPoly2 = new JsgPolygon( false, 'JsGraph.WorkPoly2' );
this.WorkRect = new JsgRect( 0, 0, 0, 0 );
this.EventHandlers = [];
this.Alpha = 1;
this.Color = 'black';
this.BgColor = 'white';
this.BgGradient = null;
this.LineWidth = 1;
this.MarkerSymbol = 'Circle';
this.MarkerSize = 8;
this.DriverMarkerSize = 8;
this.TextRendering = 'canvas'; // canvas, html
this.TextCanvasRendering = true;
this.TextClass = '';
this.TextFont = 'Arial';
this.TextSize = 15; // as int in pixel
this.TextRotation = 0; // in rad or deg
this.CanvasFontSize = 15;
this.CTextCurrFontVers = 0;
this.CTextLastFontVers = -1;
this.TextColor = 'black';
this.FontStyle = 'normal';
this.FontWeight = 'normal';
this.TextHAlign = 'center';
this.TextVAlign = 'middle';
this.TextHPad = 0;
this.TextVPad = 0;
this.CanvasTextHPad = 0;
this.CanvasTextVPad = 0;
this.LineHeight = 0;
this.CanvasLineHeight = 0;
this.LineJoin = 'round';
this.LineCap = 'butt';
this.ScaleRef = 800;
this.AutoScalePix = false;
this.LimitScalePix = true;
this.ScalePixInt = false;
this.MinLineWidth = 0.01;
this.MinTextSize = 1;
this.MinMarkerSize = 1;
this.CurvePrecision = 0.25; // pixel
this.MaxCurveSegments = 1024; // 8192 must be 4*2^n with n > 1
this.NumBezierSegments = 64;
this.DisableNativeArc = false;
this.DisableNativeBezier = false;
this.SavedAttrs = null;
this.SavedDefaultAttrs = null;
this.PenDown = false;
this.IsPathOpen = false;
this.CurrPath = [];
this.CurrPathSize = 0;
this.CommonPathElePool = [];
this.CommonPathElePoolSize = 0;
this.ArcPathElePool = [];
this.ArcPathElePoolSize = 0;
this.BezierPathElePool = [];
this.BezierPathElePoolSize = 0;
this.ContainerDiv = null;
this.ClippingDiv = null;
this.Canvas = null;
this.Context2D = null;
this.HtmlTextHandler = null;
this.IsResettingAll = false;
this.DrawTime = 50;
this.ResizeTimer = null; // timer
this.DrawTimer = null; // timer
this.DeferedDrawTimer = null; // timer
this.LastContWidthDrawn = 0;
this.LastContWidth = 0;
this.LastPixelRatioDrawn = 0;
this.LastPixelRatioOnResize = 0;
this.LastCanvasWidth = 0;
this.DrawingCount = 0; // incremeted in BeginDrawing, decremented in EndDrawing
this.DrawPending = 0;
}
JsGraph.prototype.SetDriverAttrs = function() {
// Init Context2D attributes with current atttributes
this.SetLineAttr( this.Color, this.LineWidth );
this.SetBgColor( this.BgColor );
this.SetTextRendering( this.TextRendering );
this.SetTextClass( '' );
this.SetTextAttr( this.TextFont, this.TextSize, this.TextColor, this.FontWeight, this.FontStyle, this.TextHAlign, this.TextVAlign, this.TextHPad, this.TextVPad, this.TextRotation );
this.SetLineHeight( this.LineHeight );
this.SetLineJoin( this.LineJoin );
this.SetLineCap( this.LineCap );
}
JsGraph.prototype.IdCounter = 0;
JsGraph.prototype.CreateCanvas = function( aParams ) {
// aParams =
// Id: String; Default 'JsGraph<n>', where <n> is a counter number
// Width: number(>0) or string; default '100%'; numbers are in pixel
// Height: number(>0) or string; default '75%'
// BorderWidth: number(>=0); default 1;
// BorderColor: string; default see class JsGraph { border-color }
// GraphClass: string; default 'JsGraph'
// GraphFormat: string; default ''; additional CSS classes
// }
JsGraph.prototype.IdCounter++;
if (xStr(aParams.Id)) {
this.Id = aParams.Id;
} else {
this.Id = 'JsGraph' + JsGraph.prototype.IdCounter;
}
this.BorderWidth = xDefNum( aParams.BorderWidth, 1 );
this.CreateDomObjects( aParams );
this.Context2D = this.Canvas.getContext('2d');
this.HtmlTextHandler = new JsgHtmlTextHandler( this.ClippingDiv, this.Canvas, this.Context2D );
// save clipping state..
this.Context2D.save();
// init transformations
this.UpdateCanvasSize();
this.SetViewport();
}
JsGraph.prototype.CreateDomObjects = function( aParams ) {
// Assert( this.Id and this.BorderWidth are defined )
// Creates this.ContainerDiv, this.ClippingDiv, this.Canvas
//
// aParams =
// Width: number(>0) or string; default '100%'; numbers are in pixel
// Height: number(>0) or string; default '75%'
// BorderColor: string; default see class JsGraph { border-color }
// GraphClass: string; default 'JsGraph'
// GraphFormat: string; default ''; additional CSS classes
// }
var width, height;
var borderColor = xDefStr( aParams.BorderColor, '' );
if (borderColor != '') borderColor = 'border-color:' + borderColor + ';';
var cssContainer = 'bdBoxSizing';
var cssClippingBox = '';
var cssCanvas = '';
var cssDefault = xDefStr( aParams.GraphClass, 'JsGraph' );
cssContainer = this.AddCssClass( cssContainer, cssDefault );
cssClippingBox = this.AddCssClass( cssClippingBox, cssDefault+'-ClippingBox' );
cssCanvas = this.AddCssClass( cssCanvas, cssDefault+'-Canvas' );
if (xStr(aParams.GraphFormat)) cssContainer = this.AddCssClass( cssContainer, aParams.GraphFormat );
if (cssContainer !== '') cssContainer = ' class="' + cssContainer + '"';
if (cssClippingBox !== '') cssClippingBox = ' class="' + cssClippingBox + '"';
if (cssCanvas !== '') cssCanvas = ' class="' + cssCanvas + '"';
var reqWidth = '100%';
var reqHeight = '75%';
if (xIsNumeric(aParams.Width ) || this.IsNumericPercent(aParams.Width )) reqWidth = aParams.Width;
if (xIsNumeric(aParams.Height) || this.IsNumericPercent(aParams.Height)) reqHeight = aParams.Height;
var commonStyle = 'margin:0;padding:0;';
var noborderStyle = 'border:none;';
if (this.IsNumericPercent(reqWidth)) {
// aWidth is a percentage number
var containerStyle = 'width:'+reqWidth+';height:100%;' + 'border-width:' + this.BorderWidth + 'px;padding:0;' + borderColor;
var clippingBoxStyle = 'width:100%;height:100%;font-size:0;line-height:0;overflow:hidden;' + commonStyle + noborderStyle;
var canvasStyle = commonStyle + noborderStyle;
if (containerStyle != '') containerStyle = ' style="' + containerStyle + '"';
if (clippingBoxStyle != '') clippingBoxStyle = ' style="' + clippingBoxStyle + '"';
if (canvasStyle != '') canvasStyle = ' style="' + canvasStyle + '"';
var s = '<div id="' + this.Id + '"' + cssContainer + containerStyle + '>';
s += '<div id="' + this.Id + '-ClippingBox"' + cssClippingBox + clippingBoxStyle + '>';
s += '<canvas id="' + this.Id + '-Canvas"' + cssCanvas + canvasStyle + '></canvas>';
s += '</div></div>';
document.writeln( s );
this.ContainerDiv = xGet( this.Id );
this.ClippingDiv = xGet( this.Id+'-ClippingBox' );
this.Canvas = xGet( this.Id+'-Canvas' );
this.LastContWidth = xWidth( this.ContainerDiv );
width = this.LastContWidth - 2 * this.BorderWidth;
height = this.ParseHWInt( reqHeight, width );
this.CanvasRatioHW = height / width;
this.Canvas.width = width;
this.Canvas.height = height;
} else {
// reqWidth is a pixel number (xIsNumeric(reqWidth) === true)
width = this.ParseHWInt( reqWidth );
height = this.ParseHWInt( reqHeight, width );
var containerStyle = 'width:'+width+'px;' + 'border-width:' + this.BorderWidth + 'px;padding:0;' + borderColor;
width -= 2 * this.BorderWidth;
height -= 2 * this.BorderWidth;
var clippingBoxStyle = 'width:'+width+'px;height:'+height+'px;font-size:0;line-height:0;overflow:hidden;' + commonStyle + noborderStyle;
var canvasStyle = commonStyle + noborderStyle;
if (containerStyle != '') containerStyle = ' style="' + containerStyle + '"';
if (clippingBoxStyle != '') clippingBoxStyle = ' style="' + clippingBoxStyle + '"';
if (canvasStyle != '') canvasStyle = ' style="' + canvasStyle + '"';
var s = '<div id="' + this.Id + '"' + cssContainer + containerStyle + '>';
s += '<div id="' + this.Id + '-ClippingBox"' + cssClippingBox + clippingBoxStyle + '>';
s += '<canvas id="' + this.Id + '-Canvas" width="' + width + 'px" height="' + height + 'px"' + cssCanvas + canvasStyle + '></canvas>';
s += '</div></div>';
document.writeln( s );
this.ContainerDiv = xGet( this.Id );
this.ClippingDiv = xGet( this.Id+'-ClippingBox' );
this.Canvas = xGet( this.Id+'-Canvas' );
}
var clippingDiv = this.ClippingDiv;
var canvas = this.Canvas;
if (!clippingDiv.style.position) clippingDiv.style.position = 'relative';
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.margin = 0;
canvas.style.padding = 0;
}
JsGraph.prototype.AddEventHandler = function( aEventType, aEventHandler, aCapture ) {
// aEventType: String; see javascript addEventListener(), e.g. 'click'
// aEventHandler: function( xEvent, JsGraph )
// aCapture; Boolean; Optional; Default = false; see addEventListener()
//
// Click event can be installed also by defining parameter OnClick on NewGraph2D()
// Note: aEventHandler is passed a xEvent object not a native event object. Second Argument is this JsGraph!
//
// use TransCnvsWinX(), TransCnvsWinY() to transform mouse coordinates to window coordinates
if (!xFunc(aEventHandler)) return;
var me = this;
xAddEvent(
this.Canvas,
aEventType,
function CB_Call_EventHandler(evnt) {
aEventHandler(evnt,me);
},
xDefBool(aCapture,false)
);
}
JsGraph.prototype.Redraw = function() {
this.Draw();
}
JsGraph.prototype.SetDrawFunc = function( aDrawFunc, bDrawNow ) {
// aDrawFunc = function( JsGraph )
this.DrawFunc = xDefAnyOrNull( aDrawFunc, null );
if (aDrawFunc && (this.DrawPending || bDrawNow)) {
this.QueueDraw();
}
}
JsGraph.prototype.SetDeferedDrawFunc = function( aDrawFunc ) {
// aDrawFunc = function( JsGraph )
if (this.DeferedDrawTimer) {
clearTimeout( this.DeferedDrawTimer );
this.DeferedDrawTimer = null;
}
this.DeferedDrawFunc = xDefAnyOrNull( aDrawFunc, null );
}
JsGraph.prototype.SetBeforeResetFunc = function( aBeforeClearFunc ) {
// aBeforeClearFunc = function( JsGraph )
// aBeforeClearFunc is called before Clear() is invoked and before DrawFunc is called after a window resize
// Use this callback to stop an asynchrone drawing process
this.BeforeResetFunc = xDefFunc( aBeforeClearFunc, null );
}
JsGraph.prototype.BeginDrawing = function() {
if (this.DrawingCount == 0) { this.DrawPending = 0; }
this.DrawingCount++;
}
JsGraph.prototype.EndDrawing = function( bEndAll ) {
if (bEndAll) { this.DrawingCount = 1; }
this.DrawingCount--;
if (this.DrawingCount < 0) { this.DrawingCount = 0; }
if (this.DrawingCount == 0 && this.DrawPending) {
this.QueueDraw();
}
}
JsGraph.prototype.CancelPendingDraws = function() {
if (this.DrawTimer) {
clearTimeout( this.DrawTimer );
this.DrawTimer = null;
}
if (this.DeferedDrawTimer) {
clearTimeout( this.DeferedDrawTimer );
this.DeferedDrawTimer = null;
}
this.DrawPending = 0;
}
JsGraph.prototype.QueueDraw = function() {
if (this.DrawTimer) {
clearTimeout( this.DrawTimer );
this.DrawTimer = null;
}
if (this.DeferedDrawTimer) {
clearTimeout( this.DeferedDrawTimer );
this.DeferedDrawTimer = null;
}
if (this.DrawFunc) {
this.DrawTimer = setTimeout( this.OnDrawFunc, 50 );
}
}
JsGraph.prototype.Draw = function() {
//console.log( 'JsGraph.Draw' );
if (this.DrawTimer) {
clearTimeout( this.DrawTimer );
this.DrawTimer = null;
}
if (this.DeferedDrawTimer) {
clearTimeout( this.DeferedDrawTimer );
this.DeferedDrawTimer = null;
}
if (!this.DrawFunc) return;
if (!xOnLoadFinished) {
this.QueueDraw();
return;
}
if (this.IsDrawing()) {
if (this.DrawPending == 0) this.DrawPending++;
this.QueueDraw();
return;
}
if (this.BeforeResetFunc) {
try {
this.BeforeResetFunc( this );
}
catch(err) { }
}
if (this.AutoReset) {
this.Reset( this.AutoClear );
if (this.ClientResetFunc) {
try {
this.ClientResetFunc( this );
}
catch(err) { }
}
}
// call callback
this.BeginDrawing();
try {
this.DrawFunc( this );
} catch(err) { }
this.EndDrawing();
if (this.DeferedDrawFunc) {
this.DeferedDrawTimer = setTimeout( this.OnDeferedDrawFunc, this.DeferedDrawTime );
}
}
JsGraph.prototype.DeferedDraw = function() {
//console.log( 'JsGraph.DeferedDraw' );
if (this.DeferedDrawTimer) {
clearTimeout( this.DeferedDrawTimer );
this.DeferedDrawTimer = null;
}
if (!this.DeferedDrawFunc || this.IsDrawing()) return;
// call callback
this.BeginDrawing();
try {
this.DeferedDrawFunc( this );
} catch(err) { }
this.EndDrawing();
}
JsGraph.prototype.IsDrawing = function() {
return this.DrawingCount;
}
JsGraph.prototype.IsDrawPending = function() {
return this.DrawPending;
}
JsGraph.prototype.IsInvalidDrawing = function() {
// Returns true if current draw operation can be aborted
// because canvas has changed and a OnDrawFunc is installed.
return (this.DrawPending > 0) && (this.DrawFunc);
}
JsGraph.prototype.InitResizeCheck = function() {
if (!this.ContainerDiv) return;
this.LastContWidthDrawn = xWidth(this.ContainerDiv);
this.LastPixelRatioDrawn = this.PixelRatio;
this.LastPixelRatioOnResize = this.PixelRatio;
}
JsGraph.prototype.CheckResizeRegularly = function() {
// check resize regularly to correct canvas size and transformations and to call for redraw
//console.log( 'JsGraph.CheckResizeRegularly' );
if (this.ResizeTimer) {
clearTimeout( this.ResizeTimer );
this.ResizeTimer = null;
}
if (!this.ContainerDiv) return;
this.UpdatePixelRatios();
var width = xWidth(this.ContainerDiv);
if (width != this.LastContWidth || this.LastPixelRatioOnResize != this.PixelRatio) {
this.LastContWidth = width;
this.LastPixelRatioOnResize = this.PixelRatio;
} else {
// only call redraw if resize is finished (LastContWidth = width)
if (this.LastContWidthDrawn != width || this.LastPixelRatioDrawn != this.PixelRatio) {
this.UpdateCanvasSize( width );
this.DeleteSnapshots();
this.DrawPending++;
this.QueueDraw();
this.LastContWidthDrawn = width;
this.LastPixelRatioDrawn = this.PixelRatio;
}
}
this.ResizeTimer = setTimeout( this.OnResizeFunc, this.DrawTime );
}
JsGraph.prototype.Reset = function( clear ) {
clear = xDefBool( clear, true );
this.IsResettingAll = true; // prevents Resetting Attrs in SetClipping()/Clip()
this.LastX = 0.0;
this.LastY = 0.0;
this.PenDown = false;
this.IsPathOpen = false;
this.CurrPathSize = 0;
this.ObjTrans.Reset();
this.ObjTransStack = [];
this.Trans = 'window';
this.CanvasTrans.Reset();
this.VpTrans.Reset();
this.WinTrans.Reset();
this.CurrTrans = this.WinTrans;
this.UpdateCanvasTrans();
this.SetViewport();
this.SetGraphClipping( false, 'canvas' );
this.ResetAttrs();
if (clear) this.Clear();
this.IsResettingAll = false;
}
JsGraph.prototype.UpdateCanvasTrans = function() {
this.CanvasTrans.Width = this.CanvasWidth;
this.CanvasTrans.Height = this.CanvasHeight;
}
JsGraph.prototype.GetObjTrans = function() {
// private
var otr = this.ObjTrans;
return (!otr.IsUnitTrans && otr.Enabled) ? otr : null;
}
JsGraph.prototype.IsNumericPercent = function( x ) {
if (!xStr(x) || x === '') return false;
var p = x.lastIndexOf('%');
if (p !== x.length-1) return false;
x = x.substr(0,p);
if (!xIsNumeric(x)) return false;
return true;
}
JsGraph.prototype.ParseHWInt = function( h, w ) {
var result;
if (xDef(w) && this.IsNumericPercent(h)) {
result = w * (parseFloat(h) / 100.0);
} else if (xStr(h)) {
result = parseFloat(h);
} else {
result = h;
}
result = Math.round( result );
if (result <= 0) retult = 1;
return result;
}
JsGraph.prototype.AddCssClass = function( css, addCss ) {
if (addCss === '') return css;
if (css !== '') css += ' ';
return css + addCss;
}
JsGraph.prototype.SetHighResolution = function( aOnOff ) {
var old = this.HighResolution;
aOnOff = xDefBool( aOnOff, true );
if (aOnOff == old) return old;
this.HighResolution = aOnOff;
this.HighResSet = false;
this.UpdateCanvasSize();
return old;
}
JsGraph.prototype.AdjustForHighResolutionDisplays = function() {
// implementation see http://www.html5rocks.com/en/tutorials/canvas/hidpi/
// Note: UpdatePixelRatios() must be called before this function
var context = this.Context2D;
var canvas = this.Canvas;
// upscale the canvas if the two ratios don't match
if (this.HighResolution && this.DevicePixelRatio !== this.CanvasPixelRatio) {
var ratio = this.PixelRatio;
var oldWidth = this.CanvasWidth;
var oldHeight = this.CanvasHeight;
if (canvas.width != oldWidth * ratio) {
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
}
xStyle( canvas, 'width', oldWidth + 'px' );
xStyle( canvas, 'height', oldHeight + 'px' );
} else {
// reset all to 1:1
var ratio = 1;
var width = this.CanvasWidth;
var height = this.CanvasHeight;
if (canvas.width != width) {
// resize canvas itself not only his style
canvas.width = width;
canvas.height = height;
}
xStyle( canvas, 'width', width + 'px' );
xStyle( canvas, 'height', height + 'px' );
}
// now scale the context to counter
// the fact that we've manually scaled
// our canvas element
context.setTransform( 1, 0, 0, 1, 0, 0 );
context.scale( ratio, ratio );
this.ContextScale = ratio;
this.HighResSet = true;
this.LastPixelRatio = this.PixelRatio;
}
JsGraph.prototype.UpdatePixelRatios = function() {
// implementation see http://www.html5rocks.com/en/tutorials/canvas/hidpi/
var context = this.Context2D;
this.DevicePixelRatio = window.devicePixelRatio || 1;
this.CanvasPixelRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
this.PixelRatio = this.DevicePixelRatio / this.CanvasPixelRatio;
}
JsGraph.prototype.UpdateCanvasSize = function( aContainerWidth ) {
if (!this.ContainerDiv) return;
if (!xDef(aContainerWidth)) aContainerWidth = xWidth(this.ContainerDiv);
this.UpdatePixelRatios();
if (this.CanvasRatioHW == 0) {
// fixed size canvas:
// get current canvas size
this.CanvasWidth = aContainerWidth - 2 * this.BorderWidth;
this.CanvasHeight = xHeight(this.ContainerDiv) - 2 * this.BorderWidth;
if (!this.HighResSet || this.PixelRatio != this.LastPixelRatio) {
this.AdjustForHighResolutionDisplays();
}
} else {
// variable size canvas:
// on percentage size resize ContainerDiv
var width = aContainerWidth - 2 * this.BorderWidth;
if (this.LastCanvasWidth == width && this.PixelRatio == this.LastPixelRatio) {
if (!this.HighResSet) {
this.AdjustForHighResolutionDisplays();
}
return;
}
var height = width * this.CanvasRatioHW;
xHeight( this.ContainerDiv, height + 2 * this.BorderWidth, true );
this.CanvasWidth = width;
this.CanvasHeight = height;
this.LastCanvasWidth = width;
this.AdjustForHighResolutionDisplays();
}
this.UpdateCanvasTrans();
this.UpdateGraphClipOuterRange();
}
JsGraph.prototype.UpdateGraphClipOuterRange = function() {
if (this.GraphClipExtend >= 0) {
var xExtend = this.CanvasWidth * this.GraphClipExtend;
if (xExtend < this.GraphClipMargin) xExtend = this.GraphClipMargin;
var yExtend = this.CanvasHeight * this.GraphClipExtend;
if (yExtend < this.GraphClipMargin) yExtend = this.GraphClipMargin;
this.GraphClipOuterXmin = -xExtend;
this.GraphClipOuterXmax = this.CanvasWidth + xExtend;
this.GraphClipOuterYmin = -yExtend;
this.GraphClipOuterYmax = this.CanvasHeight + yExtend;
} else {
// debug: make inner clip range smaller by extend and set outer clip range to canvas - GraphClipMargin
var xExtend = -this.CanvasWidth * this.GraphClipExtend / 2;
if (xExtend < this.GraphClipMargin) xExtend = this.GraphClipMargin;
var yExtend = -this.CanvasHeight * this.GraphClipExtend / 2;
if (yExtend < this.GraphClipMargin) yExtend = this.GraphClipMargin;
this.GraphClipInnerXmin = xExtend;
this.GraphClipInnerXmax = this.CanvasWidth - xExtend;
this.GraphClipInnerYmin = yExtend;
this.GraphClipInnerYmax = this.CanvasHeight - yExtend;
xExtend *= 0.8; yExtend *= 0.8;
this.GraphClipOuterXmin = xExtend;
this.GraphClipOuterXmax = this.CanvasWidth - xExtend;
this.GraphClipOuterYmin = yExtend;
this.GraphClipOuterYmax = this.CanvasHeight - yExtend;
}
}
JsGraph.prototype.Clear = function() {
this.Context2D.clearRect( 0, 0, this.CanvasWidth, this.CanvasHeight );
this.HtmlTextHandler.Clear();
}
JsGraph.prototype.DeleteSnapshots = function() {
this.Snapshots = [];
}
JsGraph.prototype.GetSnapshot = function( id ) {
if (this.Snapshots[id]) return this.Snapshots[id];
return null;
}
JsGraph.prototype.MakeSnapshot = function( id, x, y, w, h ) {
// or MakeSnapshot( id ) for whole canvas
// or MakeSnapshot( id, 'viewport' ) for viewport
var pixelRatio = this.DevicePixelRatio;
if (!xDef(x)) {
x = 0;
y = 0;
w = Math.floor( this.CanvasWidth * pixelRatio );
h = Math.floor( this.CanvasHeight * pixelRatio );
} else if (xStr(x)) {
var box = this.GetViewportDeviceRect();
x = box.x;
y = box.y;
w = box.w;
h = box.h;
} else {
x = Math.floor( x * pixelRatio );
y = Math.floor( y * pixelRatio );
w = Math.floor( w * pixelRatio );
h = Math.floor( h * pixelRatio );
}
var snapshot = new JsgSnapshot( x, y, w, h, this.Canvas );
if (!snapshot.ImageData) return; // canvas buffer could not be created, discard snapshot
this.Snapshots[id] = snapshot;
}
JsGraph.prototype.DrawSnapshot = function( id, clear ) {
clear = xDefBool( clear, true );
var snapshot = this.GetSnapshot( id );
if (!snapshot) return false;
var ctx = this.Context2D;
var pixelRatio = this.DevicePixelRatio;
var x = snapshot.x / pixelRatio;
var y = snapshot.y / pixelRatio;
var w = snapshot.w / pixelRatio;
var h = snapshot.h / pixelRatio;
var oldAlpha = ctx.globalAlpha;
ctx.globalAlpha = 1;
ctx.beginPath();
if (clear) ctx.clearRect( x, y, w, h );
ctx.drawImage( snapshot.ImageData, x, y, w, h );
ctx.globalAlpha = oldAlpha;
return true;
}
// Transformations
JsGraph.prototype.SetAngleMeasure = function( am ) {
// am: string 'deg' or 'rad'; init = 'deg'
// returns previsous angle measure
var old = this.AngleMeasure;
if (am == 'rad') {
this.AngleMeasure = 'rad';
} else {
this.AngleMeasure = 'deg';
}
return old;
}
JsGraph.prototype.ResetTrans = function() {
this.ObjTrans.Reset();
return this;
}
JsGraph.prototype.SaveTrans = function( reset ) {
// pushes a copy of object trans onto a stack and returns a reference to this copy
// the returned obj trans can be used in SetTrans()
// if reset == true then function ResetTrans() is called
var copyTrans = this.ObjTrans.Copy();
this.ObjTransStack.push( copyTrans );
if (reset) this.ObjTrans.Reset();
return copyTrans;
}
JsGraph.prototype.RestoreTrans = function() {
if (this.ObjTransStack.length > 0) this.ObjTrans = this.ObjTransStack.pop();
}
JsGraph.prototype.TransMove = function( x, y ) {
if (JsgVect2.Ok(x)) return this.TransMove( x[0], x[1] );
this.ObjTrans.Move( x, y );
return this;
}
JsGraph.prototype.TransScale = function( sx, sy ) {
if (JsgVect2.Ok(sx)) return this.TransScale( sx[0], sx[1] );
this.ObjTrans.Scale( sx, sy );
return this;
}
JsGraph.prototype.TransRotate = function( ang ) {
this.ObjTrans.Rotate( this.AngleToRad(ang) );
return this;
}
JsGraph.prototype.TransRotateAtPoint = function( x, y, ang ) {
if (JsgVect2.Ok(x)) return this.TransRotateAtPoint( x[0], x[1], y );
this.ObjTrans.Move( -x, -y );
this.ObjTrans.Rotate( this.AngleToRad(ang) );
this.ObjTrans.Move( x, y );
return this;
}
JsGraph.prototype.AddTrans = function( mat ) {
// mat: 2x3 or 3x3 array
this.ObjTrans.AddTrans( mat );
return this;
}
JsGraph.prototype.ObjTransPoly = function( poly ) {
// poly: JsgPolygon
// applies object transformations to poly
var otr = this.GetObjTrans();
if (!otr) return;
var xArr = poly.X;
var yArr = poly.Y;
var size = poly.Size;
for (var i = 0; i < size; i++) {
otr.TransXY( xArr[i], yArr[i] );
xArr[i] = otr.x;
yArr[i] = otr.y;
}
}
JsGraph.prototype.TransPoly = function( poly ) {
// poly: JsgPolygon
// applies all transformations to poly; returned poly is in viewport coordinates
var ctr = this.CurrTrans;
var otr = this.GetObjTrans();
var xArr = poly.X;
var yArr = poly.Y;
var size = poly.Size;
for (var i = 1; i < size; i++ ) {
ctr.ObjTransXY( otr, xArr[i], yArr[i] );
xArr[i] = ctr.x;
yArr[i] = ctr.y;
}
}
JsGraph.prototype.ObjTransXY = function( x, y ) {
// x, y: number; position in current coordinate system
// applies object transformation to x, y
// returns this.ObjTrans.x, this.ObjTrans.y; this.ObjTrans: JsgTrans2D
var otr = this.ObjTrans;
otr.TransXY( x, y );
return otr;
}
JsGraph.prototype.TransXY = function( x, y ) {
// x, y: number; position in current coordinate system
// applies all transformations to poly; returned poly is in viewport coordinates
// returns this.CurrTrans.x, this.CurrTrans.y; this.CurrTrans: JsgTrans
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), x, y );
return ctr;
}
JsGraph.prototype.SelectTrans = function( aTrans ) {
// aTrans: string = 'canvas', 'viewport', 'window'; init = 'window'
if (this.Trans == aTrans || !this.TransByName[aTrans]) return this.Trans;
var oldTrans = this.Trans;
this.CurrTrans = this.TransByName[aTrans];
this.Trans = aTrans;
return oldTrans;
}
JsGraph.prototype.SetViewport = function( aX, aY, aWidth, aHeight, bScalePix, bClip ) {
var doClip = xDef( aX );
aWidth = xDefNum( aWidth, 0 );
aHeight = xDefNum( aHeight, 0 );
aX = xDefNum( aX, 0 );
aY = xDefNum( aY, 0 );
bScalePix = xDefBool( bScalePix, false );
bClip = xDefBool( bClip, false );
if (bScalePix) {
aX = this.ScalePix( aX, this.ScalePixInt );
aY = this.ScalePix( aY, this.ScalePixInt );
if (aWidth < 0) aWidth = this.ScalePix( aWidth, this.ScalePixInt );
if (aHeight < 0) aHeight = this.ScalePix( aHeight, this.ScalePixInt );
}
this.VpXmin = aX;
this.VpYmin = aY;
if (aWidth <= 0) {
this.VpWidth = this.CanvasWidth + aWidth - aX;
} else {
this.VpWidth = aWidth;
}
if (aHeight <= 0) {
this.VpHeight = this.CanvasHeight + aHeight - aY;
} else {
this.VpHeight = aHeight;
}
this.VpInnerWidth = this.VpWidth - 1;
this.VpInnerHeight = this.VpHeight - 1;
this.SetViewportTrans();
this.SetWindow();
this.ResetInnerClipRange();
if (doClip) {
if (bClip) {
this.SetClipping( 'viewport' );
} else {
this.SetClipping( 'canvas' );
}
}
// debug:
if (this.GraphClipExtend < 0) this.UpdateGraphClipOuterRange();
}
JsGraph.prototype.ResetInnerClipRange = function() {
// Init GraphClipInnerRange
this.GraphClipInnerXmin = -this.GraphClipMargin;
this.GraphClipInnerXmax = this.CanvasWidth + this.GraphClipMargin;
this.GraphClipInnerYmin = -this.GraphClipMargin;
this.GraphClipInnerYmax = this.CanvasHeight + this.GraphClipMargin;
}
JsGraph.prototype.SetViewportRel = function( aLeft, aTop, aRight, aBottom, bScalePix, bClip ) {
aLeft = xDefNum( aLeft, 0 );
aTop = xDefNum( aTop, aLeft );
aRight = xDefNum( aRight, aLeft );
aBottom = xDefNum( aBottom, aTop );
bScalePix = xDefBool( bScalePix, true );
bClip = xDefBool( bClip, true );
if (bScalePix) {
aLeft = this.ScalePix( aLeft, this.ScalePixInt );
aTop = this.ScalePix( aTop, this.ScalePixInt );
aRight = this.ScalePix( aRight, this.ScalePixInt );
aBottom = this.ScalePix( aBottom, this.ScalePixInt );
}
this.VpWidth = this.VpWidth - aLeft - aRight;
this.VpHeight = this.VpHeight - aTop - aBottom;
this.VpXmin = this.VpXmin + aLeft;
this.VpYmin = this.VpYmin + aTop;
this.VpInnerWidth = this.VpWidth - 1;
this.VpInnerHeight = this.VpHeight - 1;
this.SetViewportTrans();
this.SetWindow();
if (bClip) {
this.SetClipping( 'viewport' );
} else {
this.SetClipping( 'canvas' );
}
}
JsGraph.prototype.SetViewportTrans = function() {
var trans = this.VpTrans;
// set trans geom
trans.Xmin = this.VpXmin;
trans.Ymin = this.VpYmin;
trans.Width = this.VpWidth;
trans.Height = this.VpHeight;
// set viewport transformation
trans.OffsetX = this.VpXmin + 0.5;
trans.OffsetY = this.VpYmin + 0.5;
trans.ScaleX = 1;
trans.ScaleY = 1;
}
JsGraph.prototype.SetWindow = function( aXmin, aYmin, aXmax, aYmax ) {
// args: real
aXmin = xDefNum( aXmin, 0 );
aYmin = xDefNum( aYmin, 0 );
aXmax = xDefNum( aXmax, 0 );
aYmax = xDefNum( aYmax, 0 );
if (aXmin == aXmax) aXmax = this.VpInnerWidth;
if (aYmin == aYmax) aYmax = this.VpInnerHeight;
this.WinXmin = aXmin;
this.WinXmax = aXmax;
this.WinYmin = aYmin;
this.WinYmax = aYmax;
this.WinWidth = aXmax - aXmin;
this.WinHeight = aYmax - aYmin;
// set trans geom
var trans = this.WinTrans;
trans.Xmin = this.WinXmin;
trans.Ymin = this.WinYmin;
trans.Width = this.WinWidth;
trans.Height = this.WinHeight;
// set window transformation
var sx = this.VpInnerWidth / this.WinWidth;
trans.ScaleX = sx;
trans.OffsetX = (-this.WinXmin * sx) + this.VpXmin + 0.5;
var sy = -(this.VpInnerHeight / this.WinHeight);
trans.ScaleY = sy;
trans.OffsetY = this.VpInnerHeight - this.WinYmin * sy + this.VpYmin + 0.5;
}
JsGraph.prototype.SetWindowWH = function( aXnull, aYnull, aWidth, aHeight ) {
// compute with and height from viewport aspect ratio
aXnull = xDefNum( aXnull, 0 );
aYnull = xDefNum( aYnull, 0 );
aWidth = xDefNum( aWidth, 0 );
aHeight = xDefNum( aHeight, 0 );
if (aWidth == 0) {
var aspectRatio = this.VpInnerWidth / this.VpInnerHeight;
aWidth = aHeight * aspectRatio;
} else if (aHeight == 0) {
var aspectRatio = this.VpInnerWidth / this.VpInnerHeight;
if (aspectRatio != 0) aHeight = aWidth / aspectRatio;
}
this.SetWindow( aXnull, aYnull, aXnull + aWidth, aYnull + aHeight );
}
JsGraph.prototype.MapWindow = function( aXcenter, aYcenter, aWidth, aHeight, aAlign ) {
// compute with and height from viewport aspect ratio
aXcenter = xDefNum( aXcenter, 0 );
aYcenter = xDefNum( aYcenter, 0 );
aWidth = xDefNum( aWidth, 0 );
aHeight = xDefNum( aHeight, 0 );
aAlign = xDefNum( aAlign, 0 );
var vpAscpectRatio = this.VpInnerWidth / this.VpInnerHeight;
if (aWidth == 0) {
aWidth = aHeight * vpAscpectRatio;
} else if (aHeight == 0) {
if (vpAscpectRatio != 0) aHeight = aWidth / vpAscpectRatio;
} else {
// assert( aWidth != 0 && aHeight != 0 )
var winAscpectRatio = aWidth / aHeight;
if (vpAspectRatio >= winAspectRatio) {
var winWidth = aHeight * vpAscpectRatio;
var padding = (winWidth - aWidth) / 2;
aXcenter -= aAlign * padding;
aWidth = winWidth;
} else {
var winHeight = aWidth / vpAscpectRatio;
var padding = (winHeight - aHeight) / 2;
aYcenter -= aAlign * padding;
aHeight = winHeight;
}
}
var xmin = aXcenter - aWidth / 2;
var ymin = aYcenter - aHeight / 2;
var xmax = xmin + aWidth;
var ymax = ymin + aHeight;
this.SetWindow( xmin, ymin, xmax, ymax );
}
JsGraph.prototype.SetClipRect = function( aX, aY, aWidth, aHeight, aTrans ) {
aX = xDefNum( aX, 0 );
aTrans = xDefStr( aTrans, '' );
var oldTrans = this.Trans;
if (aTrans != '') {
this.SelectTrans( aTrans );
}
var otr = this.ObjTrans;
var enableObjTrans = (this.Trans == 'window');
var oldEnable = otr.Enable( enableObjTrans );
this.OpenPath();
this.RectWH( aX, aY, aWidth, aHeight );
this.Clip();
if (this.Trans == 'viewport') {
// Init GraphClipInnerRange
this.GraphClipInnerXmin = this.VpXmin - this.GraphClipMargin;
this.GraphClipInnerXmax = this.VpWidth + this.GraphClipMargin;
this.GraphClipInnerYmin = this.VpYmin - this.GraphClipMargin;
this.GraphClipInnerYmax = this.VpHeight + this.GraphClipMargin;
} else {
// this.Trans is window or canvas
this.ResetInnerClipRange();
}
otr.Enable( oldEnable );
if (aTrans != '') {
this.SelectTrans( oldTrans );
}
}
JsGraph.prototype.SetClipping = function( aClipRange ) {
aClipRange = xDefStr( aClipRange, 'canvas' );
if (aClipRange == 'window') {
this.SetClipRect( this.WinXmin, this.WinYmin, this.WinWidth, this.WinHeight, 'window' );
} else if (aClipRange == 'viewport') {
this.SetClipRect( this.VpXmin, this.VpYmin, this.VpWidth, this.VpHeight, 'canvas' );
} else {
this.SetClipRect( 0, 0, this.CanvasWidth, this.CanvasHeight, 'canvas' );
}
}
JsGraph.prototype.SetGraphClipping = function( clipping, clipRange, extendFactor ) {
// clipping = true -> clips graphic elements before clipping at canvas clipping range to account for
// problems when drawing graphics much greater then canvas size
// extendFactor default is 1 which means 1 times the canvas size in each direction
// if exendFactor is not defined, doesnt change it
// note: negative extendFactor can be used to debug clipping
this.GraphClipEnabled = xDefBool( clipping, true );
if (xStr(clipRange) && clipRange != '') this.SetClipping( clipRange );
if (xNum(extendFactor)) this.GraphClipExtend = extendFactor;
this.UpdateGraphClipOuterRange();
}
JsGraph.prototype.SetAutoScalePix = function( bAutoScale ) {
bAutoScale = xDefBool( bAutoScale, true );
var old = this.AutoScalePix;
this.AutoScalePix = bAutoScale;
return old;
}
JsGraph.prototype.SetLimitScalePix = function( bLimtiScalePix ) {
bLimtiScalePix = xDefBool( bLimtiScalePix, true );
var old = this.LimtiScalePix;
this.LimtiScalePix = bLimtiScalePix;
return old;
}
JsGraph.prototype.SetScalePixInt = function( bScalePixInt ) {
bScalePixInt = xDefBool( bScalePixInt, false );
var old = this.ScalePixInt;
this.ScalePixInt = bScalePixInt;
return old;
}
JsGraph.prototype.SetScaleRef = function( aScaleRef, bLimitScalePix, bAutoScalePix, bScalePixInt ) {
// or SetScaleRef( JsgAttrsDef )
if (xObj(aScaleRef)) {
this.SetScaleRef( aScaleRef.ScaleRef, aScaleRef.LimitScalePix, aScaleRef.AutoScalePix, aScaleRef.ScalePixInt );
return;
}
if (xNum(aScaleRef)) {
this.ScaleRef = aScaleRef;
this.SavedDefaultAttrs.ScaleRef = aScaleRef;
}
if (xBool(bLimitScalePix)) {
this.LimitScalePix = bLimitScalePix;
this.SavedDefaultAttrs.LimitScalePix = bLimitScalePix;
}
if (xBool(bAutoScalePix)) {
this.AutoScalePix = bAutoScalePix;
this.SavedDefaultAttrs.AutoScalePix = bAutoScalePix;
}
if (xBool(bScalePixInt)) {
this.ScalePixInt = bScalePixInt;
this.SavedDefaultAttrs.ScalePixInt = bScalePixInt;
}
}
JsGraph.prototype.GetPixScaling = function() {
var r = this.CanvasWidth / this.ScaleRef;
if (this.LimitScalePix && r > 1) r = 1;
return r;
}
JsGraph.prototype.ScalePix = function( aSize, bInt ) {
var m = aSize < 0 ? -1 : 1;
var r = m * aSize * this.GetPixScaling();
if (bInt) {
r = Math.round( r );
if (r < 1) r = 1;
}
return m * r;
}
JsGraph.prototype.ScalePixI = function( aSize ) {
return this.ScalePix( aSize, true );
}
JsGraph.prototype.ScalePixMax = function( aSize, aMaxSize, bInt ) {
var m = aSize < 0 ? -1 : 1;
var r = m * aSize * this.GetPixScaling();
if (r > aMaxSize) r = aMaxSize;
if (bInt) {
r = Math.round( r );
if (r < 1) r = 1;
}
return m * r;
}
JsGraph.prototype.ScalePixMaxI = function( aSize, aMaxSize ) {
return this.ScalePixMax( aSize, aMaxSize, true );
}
JsGraph.prototype.ScalePixMin = function( aSize, aMinSize, bInt ) {
var m = aSize < 0 ? -1 : 1;
var r = m * aSize * this.GetPixScaling();
if (r < aMinSize) r = aMinSize;
if (bInt) {
r = Math.round( r );
if (r < 1) r = 1;
}
return m * r;
}
JsGraph.prototype.ScalePixMinI = function( aSize, aMinSize ) {
return this.ScalePixMin( aSize, aMinSize, true );
}
JsGraph.prototype.ScalePixMinMax = function( aSize, aMinSize, aMaxSize, bInt ) {
var m = aSize < 0 ? -1 : 1;
var r = m * aSize * this.GetPixScaling();
if (r < aMinSize) r = aMinSize;
if (r > aMaxSize) r = aMaxSize;
if (bInt) {
r = Math.round( r );
if (r < 1) r = 1;
}
return m * r;
}
JsGraph.prototype.ScalePixMinMaxI = function( aSize, aMinSize, aMaxSize ) {
return this.ScalePixMinMax( aSize, aMinSize, aMaxSize, true );
}
JsGraph.prototype.MinSize = function( aSize, aMinSize ) {
return (aSize < aMinSize) ? aMinSize : aSize;
}
JsGraph.prototype.MaxSize = function( aSize, aMaxSize ) {
return (aSize > aMaxSize) ? aMaxSize : aSize;
}
JsGraph.prototype.MinMaxSize = function( aSize, aMinSize, aMaxSize ) {
var r = aSize;
if (r < aMinSize) r = aMinSize;
if (r > aMaxSize) r = aMaxSize;
return r;
}
JsGraph.prototype.Limit01 = function( x ) {
return (x < 0) ? 0 : ((x > 1) ? 1 : x);
}
JsGraph.prototype.ScaleToTic = function( aValue, aTic ) {
var v = (Math.round(Math.abs(aValue)/aTic + 0.3) + 0.5) * aTic
return (aValue < 0) ? -v : v;
}
JsGraph.prototype.ScaleWinX = function() {
return Math.abs( this.WinTrans.ScaleX );
}
JsGraph.prototype.ScaleWinY = function() {
return Math.abs( this.WinTrans.ScaleY );
}
JsGraph.prototype.TransWinVpX = function( x ) {
// transforms window to viewport
var cx = this.WinTrans.TransX( x );
return this.VpTrans.InvTransX( cx );
}
JsGraph.prototype.TransWinVpY = function( y ) {
var cy = this.WinTrans.TransY( y );
return this.VpTrans.InvTransY( cy );
}
JsGraph.prototype.TransWinCnvsX = function( x ) {
// transforms window to canvas
return this.WinTrans.TransX( x );
}
JsGraph.prototype.TransWinCnvsY = function( y ) {
return this.WinTrans.TransY( y );
}
JsGraph.prototype.TransVpCnvsX = function( x ) {
// transforms viewport to canvas
return this.VpTrans.TransX( x );
}
JsGraph.prototype.TransVpCnvsY = function( y ) {
return this.VpTrans.TransY( y );
}
JsGraph.prototype.TransVpWinX = function( x ) {
// transforms viewport to window
var cx = this.VpTrans.TransX( x );
return this.WinTrans.InvTransX( cx );
}
JsGraph.prototype.TransVpWinY = function( y ) {
var cy = this.VpTrans.TransY( y );
return this.WinTrans.InvTransY( cy );
}
JsGraph.prototype.TransCnvsWinX = function( x ) {
return this.WinTrans.InvTransX( x );
}
JsGraph.prototype.TransCnvsWinY = function( y ) {
return this.WinTrans.InvTransY( y );
}
JsGraph.prototype.TransCnvsVpX = function( x ) {
// transforms canvas to viewport
return this.VpTrans.InvTransX( x );
}
JsGraph.prototype.TransCnvsVpY = function( y ) {
return this.VpTrans.InvTransY( y );
}
// attribute management
JsGraph.prototype.GetAttrs = function() {
return new JsgAttrs( this );
}
JsGraph.prototype.SetAttrs = function( aAttrs ) {
// aAttrs as JsgAttrs
if (!xObj(aAttrs)) return;
if (aAttrs.Reset) this.ResetAttrs();
this.SetScaleRef( aAttrs );
if (xDef(aAttrs.CurvePrecision)) this.CurvePrecision = aAttrs.CurvePrecision;
if (xDef(aAttrs.AngleMeasure)) this.SetAngleMeasure( aAttrs.AngleMeasure );
if (xDef(aAttrs.ObjTrans)) this.ObjTrans.CopyFrom( aAttrs.ObjTrans );
if (xDef(aAttrs.Trans)) this.SelectTrans( aAttrs.Trans );
if (xDef(aAttrs.Alpha)) this.SetAlpha( aAttrs.Alpha );
if (xDef(aAttrs.LineJoin)) this.SetLineJoin( aAttrs.LineJoin );
if (xDef(aAttrs.LineCap)) this.SetLineCap( aAttrs.LineCap );
if (xDef(aAttrs.Color)) this.SetColor( aAttrs.Color );
if (xObj(aAttrs.BgGradient)) {
this.SetBgColor( aAttrs.BgGradient );
} else if (xDef(aAttrs.BgColor)) {
this.SetBgColor( aAttrs.BgColor );
}
if (xDef(aAttrs.LineWidth)) this.SetLineWidth( aAttrs.LineWidth );
if (xDef(aAttrs.MarkerSymbol)) this.SetMarkerSymbol( aAttrs.MarkerSymbol );
if (xDef(aAttrs.MarkerSize)) this.SetMarkerSize( aAttrs.MarkerSize );
if (xDef(aAttrs.TextRendering)) this.SetTextRendering( aAttrs.TextRendering );
if (xDef(aAttrs.TextClass)) this.SetTextClass( aAttrs.TextClass );
if (xDef(aAttrs.TextFont)) this.SetTextFont( aAttrs.TextFont );
if (xDef(aAttrs.TextSize)) this.SetTextSize( aAttrs.TextSize );
if (xDef(aAttrs.TextRotation)) this.SetTextRotation( aAttrs.TextRotation );
if (xDef(aAttrs.TextColor)) this.SetTextColor( aAttrs.TextColor );
if (xDef(aAttrs.FontStyle)) this.SetFontStyle( aAttrs.FontStyle );
if (xDef(aAttrs.FontWeight)) this.SetFontWeight( aAttrs.FontWeight );
if (xDef(aAttrs.TextHAlign)) this.SetTextHAlign( aAttrs.TextHAlign );
if (xDef(aAttrs.TextVAlign)) this.SetTextVAlign( aAttrs.TextVAlign );
if (xDef(aAttrs.TextHPad)) this.SetTextPadding( aAttrs.TextHPad, this.TextVPad );
if (xDef(aAttrs.TextVPad)) this.SetTextPadding( this.TextHPad, aAttrs.TextVPad );
if (xDef(aAttrs.LineHeight)) this.SetLineHeight( aAttrs.LineHeight );
}
JsGraph.prototype.SaveAttrs = function() {
this.SavedAttrs = this.GetAttrs();
}
JsGraph.prototype.RestoreAttrs = function() {
if (!this.SavedAttrs) return;
this.SetAttrs( this.SavedAttrs );
}
JsGraph.prototype.SaveDefaultAttrs = function() {
// private function
this.SavedDefaultAttrs = this.GetAttrs();
}
JsGraph.prototype.ResetAttrs = function() {
// private function
this.SetAttrs( this.SavedDefaultAttrs );
}
// Utility functions
JsGraph.prototype.BoxWHOverlapping = function( aBox1, aBox2 ) {
// aBox1, aBox2: JsgRect
// returns true if boxes overlap
if (!aBox1 || !aBox2) return false;
var xmin1 = aBox1.x;
var xmax1 = aBox1.x + aBox1.w;
if (xmin1 > xmax1) { var tmp = xmin1; xmin1 = xmax1; xmax1 = tmp; }
var xmin2 = aBox2.x;
var xmax2 = aBox2.x + aBox2.w;
if (xmin2 > xmax2) { var tmp = xmin2; xmin2 = xmax2; xmax2 = tmp; }
if (xmax1 < xmin2 || xmax2 < xmin1) return false;
var ymin1 = aBox1.y;
var ymax1 = aBox1.y + aBox1.h;
if (ymin1 > ymax1) { var tmp = ymin1; ymin1 = ymax1; ymax1 = tmp; }
var ymin2 = aBox2.y;
var ymax2 = aBox2.y + aBox2.h;
if (ymin2 > ymax2) { var tmp = ymin2; ymin2 = ymax2; ymax2 = tmp; }
if (ymax1 < ymin2 || ymax2 < ymin1) return false;
return true;
}
JsGraph.prototype.MapToRange = function( val, range ) {
// maps val to the range (0..range]
var absVal = Math.abs(val);
var n = Math.floor( absVal / range );
var newVal = absVal - n * range;
if (val < 0) {
newVal = range - newVal;
if (newVal >= range) newVal -= range;
} else {
if (newVal < 0) newVal += range;
}
return newVal;
}
JsGraph.prototype.NormalizeAngles = function( angles ) {
// angles = { delta, start, end }
// maps angles.start and angles.end to range [-2Pi..2Pi] so
// that angles.end can be reached from angles.start in direction angles.delta
// and maximal difference between angles is 2Pi
// note: aligned angles correspond to an arc of lengt zero, not a circle!
var Pi2 = Math.PI * 2;
if (angles.delta >= 0) {
var angleDiff = angles.end - angles.start;
if (angleDiff > 0) {
// angleDiff > 0 && delta > 0
if (angleDiff > Pi2) angleDiff = Pi2;
angles.start = this.MapToRange( angles.start, Pi2 );
angles.end = angles.start + angleDiff;
if (angles.end > Pi2) {
angles.start -= Pi2;
angles.end -= Pi2;
}
} else {
// angleDiff < 0 && delta > 0
angles.start = this.MapToRange( angles.start, Pi2 );
angles.end = this.MapToRange( angles.end, Pi2 );
if (angles.start > angles.end) angles.start -= Pi2;
}
} else {
// delta < 0
var angleDiff = angles.end - angles.start;
if (angleDiff < 0) {
// angleDiff < 0 && delta < 0
if (angleDiff < -Pi2) angleDiff = -Pi2;
angles.start = this.MapToRange( angles.start, Pi2 );
angles.end = angles.start + angleDiff;
} else {
// angleDiff > 0 && delta < 0
angles.start = this.MapToRange( angles.start, Pi2 );
angles.end = this.MapToRange( angles.end, Pi2 );
if (angles.end > angles.start) angles.end -= Pi2;
}
}
}
JsGraph.prototype.NormalizeAngle = function( angle ) {
return this.MapToRange( angle, 2*Math.PI );
}
JsGraph.prototype.CompDeltaAngle = function( radius, precision ) {
// radius > 0, precision > 0
// returns a delta angle > 0 in range (Pi/4..Pi/MaxCurveSegments), so
// that the pixel error for an arc with radius is less than precision and
// angle is a an integer fraction of Pi/2.
var da = 2 * Math.acos( (radius - precision) / radius );
da = (Math.PI / 2) / (Math.floor( Math.PI / 2 / da ) + 1);
if (da > Math.PI / 4) da = Math.PI / 4;
if (this.MaxCurveSegments > 0 && da < Math.PI / this.MaxCurveSegments) da = Math.PI / this.MaxCurveSegments;
//var nSeg = Math.PI / da * 2; // debug
return da;
}
JsGraph.prototype.MakeUnityArcPolygon = function( aAngles ) {
// computes an unity arc from start to end angle with delta increment and direction
// aAngles = { delta, start, end }; use this.NormalizeAngles() for appropriate angles
// returns this.WorkPoly: JsgPolygon
// note: arc points are aligned to angles 0, Pi/2, Pi, 3/2Pi
var poly = this.WorkPoly.Reset();
var sin = Math.sin;
var cos = Math.cos;
if (aAngles.delta > 0) {
var delta = aAngles.delta - this.MapToRange( aAngles.start, aAngles.delta );
if (delta == 0) delta = aAngles.delta;
var currAng = aAngles.start;
var lastAng = aAngles.end - aAngles.delta/1000;
while (currAng < lastAng) {
poly.AddPoint( cos(currAng), sin(currAng) );
currAng += delta;
delta = aAngles.delta;
}
poly.AddPoint( cos(aAngles.end), sin(aAngles.end) );
} else if (aAngles.delta < 0) {
var delta = - this.MapToRange( aAngles.start, -aAngles.delta );
if (delta == 0) delta = aAngles.delta;
var currAng = aAngles.start;
var lastAng = aAngles.end + aAngles.delta/1000;
while (currAng > lastAng) {
poly.AddPoint( cos(currAng), sin(currAng) );
currAng += delta;
delta = aAngles.delta;
}
poly.AddPoint( cos(aAngles.end), sin(aAngles.end) );
}
return poly;
}
// Styles
JsGraph.prototype.SetAlpha = function( a ) {
// a: number 0..1, 0 = invisible, 1 = not tranparent
this.Alpha = this.MinMaxSize( xDefNum( a, 1 ), 0, 1 );
this.Context2D.globalAlpha = this.Alpha;
}
JsGraph.prototype.SetLineJoin = function( j ) {
// j: 'miter', 'round', 'bevel' (Default 'miter')
this.LineJoin = j;
this.Context2D.lineJoin = j;
}
JsGraph.prototype.SetLineCap = function( c ) {
// c: 'butt', 'round', 'square' (Default 'butt')
this.LineCap = c;
this.Context2D.lineCap = c;
}
JsGraph.prototype.SetLineAttr = function( color, width ) {
// color: string CSS: 'red', '#f00', '#ff0000' or JsgGradient
// width: int >= 0
if (xAny(color)) this.SetColor( color );
if (xAny(width)) this.SetLineWidth( width );
}
JsGraph.prototype.SetAreaAttr = function( bgColor, borderColor, borderWidth ) {
// bgColor: string CSS: 'red', '#f00', '#ff0000' or JsgGradient
// borderColor: string CSS: 'red', '#f00', '#ff0000'
// borderWidth: int >= 0
if (xAny(bgColor)) this.SetBgColor( bgColor );
if (xAny(borderColor)) this.SetColor( borderColor );
if (xAny(borderWidth)) this.SetLineWidth( borderWidth );
}
JsGraph.prototype.SetMarkerAttr = function( aSymbolName, size, borderColor, bgColor, borderWidth ) {
// aSymbolName: string: 'ArrowLeft'...'Star'
// size: real or int marker size in pixel
// bgColor: string CSS: 'red', '#f00', '#ff0000' or JsgGradient
// borderColor: string CSS: 'red', '#f00', '#ff0000'
// borderWidth: int >= 0
if (xAny(aSymbolName)) this.SetMarkerSymbol( aSymbolName );
if (xAny(size)) this.SetMarkerSize( size );
this.SetAreaAttr( bgColor, borderColor, borderWidth );
}
JsGraph.prototype.SetTextAttr = function( aFont, aSize, aColor, aWeight, aStyle, aHAlign, aVAlign, aHPad, aVPad, aRot ) {
if (xAny(aFont)) this.SetTextFont( aFont );
if (xAny(aSize)) this.SetTextSize( aSize );
if (xAny(aRot)) this.SetTextRotation( aRot );
if (xAny(aColor)) this.SetTextColor( aColor );
if (xAny(aWeight)) this.SetFontWeight( aWeight );
if (xAny(aStyle)) this.SetFontStyle( aStyle );
if (xAny(aHAlign)) this.SetTextHAlign( aHAlign );
if (xAny(aVAlign)) this.SetTextVAlign( aVAlign );
if (xAny(aHPad)) this.SetTextPadding( aHPad, aVPad );
}
JsGraph.prototype.ClearTextAttr = function( ) {
this.SetTextAttr( '', 0, '', '', '', '', '', 0, 0 );
this.SetLineHeight( -1 );
}
JsGraph.prototype.SetColor = function( color ) {
// color: string Css: 'red', '#ff0000', 'rgb(...)', 'rgba(...)' or JsgColor
color = xDefAny( color, this.SavedDefaultAttrs.Color );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
this.Color = color;
this.Context2D.strokeStyle = this.Color;
}
JsGraph.prototype.SetBgColor = function( color ) {
// color: string Css: 'red', '#ff0000' or JsgColor or JsgGradient
color = xDefAny( color, this.SavedDefaultAttrs.BgColor );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
if (xStr(color)) {
this.BgColor = color;
this.BgGradient = null;
this.Context2D.fillStyle = this.BgColor;
} else if (JsgGradient.Ok(color)) {
this.BgGradient = color;
this.Context2D.fillStyle = color.CanvasGradient;
}
}
JsGraph.prototype.CreateLinearGradient = function( aGradientDef ) {
// aGradientDef = { X1, Y1, X2, Y2, Stops: [ { Pos, Color }, ... ] }
// returns JsgGradient of Type = 'linear'
aGradientDef.X1 = xDefNum( aGradientDef.X1, 0 );
aGradientDef.Y1 = xDefNum( aGradientDef.Y1, 0 );
aGradientDef.X2 = xDefNum( aGradientDef.X2, aGradientDef.X1 );
aGradientDef.Y2 = xDefNum( aGradientDef.Y2, aGradientDef.Y1 );
aGradientDef.Stops = xArray(aGradientDef.Stops) ? aGradientDef.Stops : [];
var ctr = this.CurrTrans;
ctr.ObjTransXY2( this.GetObjTrans(), aGradientDef.X1, aGradientDef.Y1, aGradientDef.X2, aGradientDef.Y2 );
var grad = this.Context2D.createLinearGradient( ctr.x1, ctr.y1, ctr.x2, ctr.y2 );
var stops = aGradientDef.Stops;
var last = stops.length-1;
for (var i = 0; i <= last; i++) {
var color = xDefAny( stops[i].Color, 'gray' );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
grad.addColorStop( xDefNum( stops[i].Pos, i/last ), color );
}
return new JsgGradient( 'linear', grad, aGradientDef );
}
JsGraph.prototype.SetLinearGradientGeom = function( aLinearGradient, aGeom ) {
// aGeom = { X1, Y1, X2, Y2 }
var gradDef = aLinearGradient.GradientDef;
gradDef.X1 = xDefNum( aGeom.X1, gradDef.X1 );
gradDef.Y1 = xDefNum( aGeom.Y1, gradDef.Y1 );
gradDef.X2 = xDefNum( aGeom.X2, gradDef.X2 );
gradDef.Y2 = xDefNum( aGeom.Y2, gradDef.Y2 );
var ctr = this.CurrTrans;
ctr.ObjTransXY2( this.GetObjTrans(), gradDef.X1, gradDef.Y1, gradDef.X2, gradDef.Y2 );
var grad = this.Context2D.createLinearGradient( ctr.x1, ctr.y1, ctr.x2, ctr.y2 );
var stops = gradDef.Stops;
var last = stops.length-1;
for (var i = 0; i <= last; i++) {
var color = xDefAny( stops[i].Color, 'gray' );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
grad.addColorStop( xDefNum( stops[i].Pos, i/last ), color );
}
aLinearGradient.CanvasGradient = grad;
if (aLinearGradient == this.BgGradient) {
this.Context2D.fillStyle = this.BgGradient.CanvasGradient;
}
}
JsGraph.prototype.CreateRadialGradient = function( aGradientDef ) {
// aGradientDef = { X1, Y1, R1, X2, Y2, R2, Stops: [ { Pos, Color }, ... ] }
// returns JsgGradient of Type = 'radial'
aGradientDef.X1 = xDefNum( aGradientDef.X1, 0 );
aGradientDef.Y1 = xDefNum( aGradientDef.Y1, 0 );
aGradientDef.R1 = xDefNum( aGradientDef.R1, 0 );
aGradientDef.X2 = xDefNum( aGradientDef.X2, aGradientDef.X1 );
aGradientDef.Y2 = xDefNum( aGradientDef.Y2, aGradientDef.Y1 );
aGradientDef.R2 = xDefNum( aGradientDef.R2, aGradientDef.R1 + 100 );
var ctr = this.CurrTrans;
var otrScaling = this.ObjTrans.MaxScaling();
ctr.ObjTransXY2( this.GetObjTrans(), aGradientDef.X1, aGradientDef.Y1, aGradientDef.X2, aGradientDef.Y2 );
var cnvsR1 = ctr.ScaleX * otrScaling * aGradientDef.R1;
var cnvsR2 = ctr.ScaleX * otrScaling * aGradientDef.R2;
var grad = this.Context2D.createRadialGradient( ctr.x1, ctr.y1, cnvsR1, ctr.x2, ctr.y2, cnvsR2 );
var stops = xDefArray( aGradientDef.Stops, [] );
var last = stops.length-1;
for (var i = 0; i <= last; i++) {
var color = xDefAny( stops[i].Color, 'gray' );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
grad.addColorStop( xDefNum( stops[i].Pos, i/last ), color );
}
return new JsgGradient( 'radial', grad, aGradientDef );
}
JsGraph.prototype.SetRadialGradientGeom = function( aRadialGradient, aGeom ) {
// aGeom = { X1, Y1, R1, X2, Y2, R2 }
var gradDef = aRadialGradient.GradientDef;
gradDef.X1 = xDefNum( aGeom.X1, gradDef.X1 );
gradDef.Y1 = xDefNum( aGeom.Y1, gradDef.Y1 );
gradDef.R1 = xDefNum( aGeom.R1, gradDef.R1 );
gradDef.X2 = xDefNum( aGeom.X2, gradDef.X2 );
gradDef.Y2 = xDefNum( aGeom.Y2, gradDef.Y2 );
gradDef.R2 = xDefNum( aGeom.R2, gradDef.R2 );
var ctr = this.CurrTrans;
var otrScaling = this.ObjTrans.MaxScaling();
ctr.ObjTransXY2( this.GetObjTrans(), gradDef.X1, gradDef.Y1, gradDef.X2, gradDef.Y2 );
var cnvsR1 = ctr.ScaleX * otrScaling * gradDef.R1;
var cnvsR2 = ctr.ScaleX * otrScaling * gradDef.R2;
var grad = this.Context2D.createRadialGradient( ctr.x1, ctr.y1, cnvsR1, ctr.x2, ctr.y2, cnvsR2 );
var stops = gradDef.Stops;
var last = stops.length-1;
for (var i = 0; i <= last; i++) {
var color = xDefAny( stops[i].Color, 'gray' );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
grad.addColorStop( xDefNum( stops[i].Pos, i/last ), color );
}
aRadialGradient.CanvasGradient = grad;
if (aRadialGradient == this.BgGradient) {
this.Context2D.fillStyle = this.BgGradient.CanvasGradient;
}
}
JsGraph.prototype.SetLineWidth = function( width ) {
// width in pixel: Number >= 0; if 0 then width internal width is set so, that 1 pixel width is drawn on all scales
width = xDefNum( width, this.SavedDefaultAttrs.LineWidth );
if (width < 0) width = 0;
this.LineWidth = width;
if (this.AutoScalePix && width > 0) width = this.ScalePixMin( width, this.MinLineWidth, this.ScalePixInt );
if (width == 0) width = 1 / this.DevicePixelRatio;
this.Context2D.lineWidth = width;
}
JsGraph.prototype.SetTextClass = function( aClassName, aClearAttrs ) {
aClassName = xDefStr( aClassName, '' );
aClearAttrs = xDefBool( aClearAttrs, false );
if (aClearAttrs) this.ClearTextAttr();
this.TextClass = aClassName;
this.HtmlTextHandler.TextClass = aClassName;
}
JsGraph.prototype.SetTextRendering = function( aRenderMethod ) {
// aRenderMethod: String = canvas, html
var oldRendering = this.TextRendering;
if (!(this.Context2D.strokeText && this.Context2D.fillText)) aRenderMethod = 'html';
if (aRenderMethod == 'html') {
this.TextRendering = 'html';
this.TextCanvasRendering = false;
} else {
this.TextRendering = 'canvas';
this.TextCanvasRendering = true;
}
return oldRendering;
}
JsGraph.prototype.SetTextFont = function( aFont ) {
// set aFont = '' if fontStyle is defined in TextClass or in another CSS
this.TextFont = xDefStr( aFont, this.SavedDefaultAttrs.TextFont );
this.HtmlTextHandler.TextStyles.fontFamily = this.TextFont;
this.CTextCurrFontVers++;
}
JsGraph.prototype.SetTextSize = function( aSize ) {
// aSize in pixel: Number
aSize = xDefNum( aSize, this.SavedDefaultAttrs.TextSize );
if (aSize < 0) aSize = 0;
this.TextSize = aSize;
if (aSize > 0) {
if (this.AutoScalePix) aSize = this.ScalePixMin( aSize, this.MinTextSize, this.ScalePixInt );
this.HtmlTextHandler.TextStyles.fontSize = aSize + 'px';
this.CanvasFontSize = aSize;
} else {
this.HtmlTextHandler.TextStyles.fontSize = '';
this.CanvasFontSize = 15;
}
this.CTextCurrFontVers++;
}
JsGraph.prototype.SetTextRotation = function( aRot ) {
aRot = xDefNum( aRot, this.SavedDefaultAttrs.TextRotation );
this.TextRotation = aRot;
}
JsGraph.prototype.SetTextColor = function( color ) {
// color: string Css: 'red', '#ff0000', 'rgb(...)', 'rgba(...)' or JsgColor
// set color = '' if color is set in TextClass or other CSS
color = xDefAny( color, this.SavedDefaultAttrs.TextColor );
if (JsgColor.Ok(color)) color = JsgColor.ToString(color);
this.TextColor = color;
this.HtmlTextHandler.TextStyles.color = this.TextColor;
}
JsGraph.prototype.SetLineHeight = function( aHeight ) {
// aHeight: Number (Pixels); aHeight = 0 -> '100%'
// set aHeight = -1 if lineHeight is set in TextClass or other CSS
aHeight = xDefNum( aHeight, this.SavedDefaultAttrs.LineHeight );
if (aHeight < 0) aHeight = -1;
this.LineHeight = aHeight;
if (aHeight > 0) {
if (this.AutoScalePix) aHeight = this.ScalePix( aHeight, this.ScalePixInt );
this.HtmlTextHandler.TextStyles.lineHeight = aHeight + 'px';
this.CanvasLineHeight = aHeight;
} else if (aHeight == 0) {
this.HtmlTextHandler.TextStyles.lineHeight = '100%';
this.CanvasLineHeight = 0;
} else {
this.HtmlTextHandler.TextStyles.lineHeight = '';
this.CanvasLineHeight = 0;
}
this.CTextCurrFontVers++;
}
JsGraph.prototype.SetFontStyle = function( aStyle, aWeight ) {
// aStyle = '', 'normal', 'italic'
// set aStyle = '' if fontStyle is defined in TextClass or other CSS
aStyle = xDefStr( aStyle, this.SavedDefaultAttrs.FontStyle );
this.FontStyle = aStyle;
this.HtmlTextHandler.TextStyles.fontStyle = aStyle;
if (xStr(aWeight)) this.SetFontWeight(aWeight);
this.CTextCurrFontVers++;
}
JsGraph.prototype.SetFontWeight = function( aWeight, aStyle ) {
// aWeigth = '', 'normal', 'bold'
// set aWeight = '' if fontWeight is defined in TextClass or other CSS
aWeight = xDefStr( aWeight, this.SavedDefaultAttrs.FontWeight );
this.FontWeight = aWeight;
this.HtmlTextHandler.TextStyles.fontWeight = aWeight;
if (xStr(aStyle)) this.SetFontStyle(aStyle);
this.CTextCurrFontVers++;
}
JsGraph.prototype.SetTextAlign = function( aHAlign, aVAlign ) {
if (xStr(aHAlign)) this.SetTextHAlign( aHAlign );
if (xStr(aVAlign)) this.SetTextVAlign( aVAlign );
}
JsGraph.prototype.SetTextHAlign = function( aAlign ) {
// aAlign: string: 'left', 'center', 'right', 'justify'
aAlign = xDefStr( aAlign, this.SavedDefaultAttrs.TextHAlign );
this.TextHAlign = aAlign;
this.HtmlTextHandler.TextStyles.textAlign = aAlign;
if (aAlign == 'justify') aAlign = 'center';
this.HtmlTextHandler.TextHAlign = aAlign;
}
JsGraph.prototype.SetTextVAlign = function( aAlign ) {
// aAlign: string: 'top', 'middle', 'bottom'
aAlign = xDefStr( aAlign, this.SavedDefaultAttrs.TextVAlign );
this.TextVAlign = aAlign;
this.HtmlTextHandler.TextVAlign = aAlign;
}
JsGraph.prototype.SetTextPadding = function( aHPad, aVPad ) {
aHPad = xDefNum( aHPad, 0 );
aVPad = xDefNum( aVPad, aHPad );
this.TextHPad = aHPad;
this.TextVPad = aVPad;
if (this.AutoScalePix) {
aHPad = this.ScalePix( aHPad, this.ScalePixInt );
aVPad = this.ScalePix( aVPad, this.ScalePixInt );
}
this.CanvasTextHPad = aHPad;
this.HtmlTextHandler.TextHPad = aHPad;
this.CanvasTextVPad = aVPad;
this.HtmlTextHandler.TextVPad = aVPad;
}
JsGraph.prototype.SetMarkerSymbol = function( aSymbolName ) {
aSymbolName = xDefStr( aSymbolName, this.SavedDefaultAttrs.MarkerSymbol );
if (!xDef(this.Markers[aSymbolName])) return;
this.MarkerSymbol = aSymbolName;
}
JsGraph.prototype.SetMarkerSize = function( aSize ) {
aSize = xDefNum( aSize, this.SavedDefaultAttrs.MarkerSize );
if (aSize < 0) aSize = 0;
this.MarkerSize = aSize;
if (this.AutoScalePix) aSize = this.ScalePixMin( aSize, this.MinMarkerSize, this.ScalePixInt );
this.DriverMarkerSize = aSize;
}
// path functions
JsGraph.prototype.OpenPath = function( penUp ) {
this.ClearPath();
this.IsPathOpen = true;
if (xDef(penUp)) this.PenDown = !penUp;
}
JsGraph.prototype.ClearPath = function() {
this.CurrPathSize = 0;
this.CommonPathElePoolSize = 0;
this.ArcPathElePoolSize = 0;
this.BezierPathElePoolSize = 0;
this.IsPathOpen = false;
}
JsGraph.prototype.Path = function( mode, clear ) {
// draws a path according to mode
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, 4 -> close path, default = 1
mode = xDefNum( mode, 1 );
clear = xDefBool( clear, true );
if (mode & 2) {
if (this.DriverDrawPath( false, true )) {
this.Context2D.fill();
}
}
if (mode & 1) {
if (this.DriverDrawPath( (mode&4) > 0, false )) {
this.Context2D.stroke();
}
}
if (clear) this.ClearPath();
}
JsGraph.prototype.Clip = function( clear ) {
// skips resetting attrs here if this.IsResttingAll is true
clear = xDefBool( clear, true );
// disable clipping for this function
var oldClipEnabled = this.GraphClipEnabled;
this.GraphClipEnabled = false;
if (this.DriverDrawPath( false, false )) {
this.Context2D.restore();
this.Context2D.save();
this.Context2D.clip();
}
if (clear) this.ClearPath();
this.GraphClipEnabled = oldClipEnabled;
// reset attrs in driver to current attrs
if (!this.IsResettingAll) this.SetDriverAttrs();
// Init GraphClipInnerRange
this.ResetInnerClipRange();
}
JsGraph.prototype.NewCommonPathEle = function( t, x, y ) {
var ele, pool = this.CommonPathElePool;
if (this.CommonPathElePoolSize < pool.length) {
ele = pool[this.CommonPathElePoolSize++];
ele.t = t; ele.x = x; ele.y = y;
} else {
ele = { t: t, x: x, y: y };
pool[this.CommonPathElePoolSize++] = ele;
}
return ele;
}
JsGraph.prototype.NewArcPathEle = function( x, y, r, sa, ea, cc ) {
var ele, pool = this.ArcPathElePool;
if (this.ArcPathElePoolSize < pool.length) {
ele = pool[this.ArcPathElePoolSize++];
ele.t = 3; ele.x = x; ele.y = y; ele.r = r; ele.sa = sa; ele.ea = ea; ele.cc = cc;
} else {
ele = { t: 3, x: x, y: y, r: r, sa: sa, ea: ea, cc: cc };
pool[this.ArcPathElePoolSize++] = ele;
}
return ele;
}
JsGraph.prototype.NewBezierPathEle = function( cx1, cy1, cx2, cy2, ex, ey ) {
var ele, pool = this.BezierPathElePool;
if (this.BezierPathElePoolSize < pool.length) {
ele = pool[this.BezierPathElePoolSize++];
ele.t = 4; ele.cx1 = cx1; ele.cy1 = cy1; ele.cx2 = cx2; ele.cy2 = cy2; ele.ex = ex; ele.ey = ey;
} else {
ele = { t: 4, cx1: cx1, cy1: cy1, cx2: cx2, cy2: cy2, ex: ex, ey: ey };
pool[this.BezierPathElePoolSize++] = ele;
}
return ele;
}
// Element-Codes: 0 = close; 1 = lineTo; 2 = moveTo; 3 = arc
JsGraph.prototype.ClosePath = function() {
this.CurrPath[this.CurrPathSize++] = this.NewCommonPathEle( 0, 0, 0 );
}
JsGraph.prototype.PathMoveTo = function( x, y ) {
this.CurrPath[this.CurrPathSize++] = this.NewCommonPathEle( 2, x, y );
}
JsGraph.prototype.PathLineTo = function( x, y ) {
this.CurrPath[this.CurrPathSize++] = this.NewCommonPathEle( 1, x, y );
}
JsGraph.prototype.PathAppendArc = function( x, y, r, sa, ea, cc, cont, close ) {
var arcStartX = x + r * Math.cos( sa );
var arcStartY = y + r * Math.sin( sa );
if (!cont) {
this.PathMoveTo( arcStartX, arcStartY );
}
this.CurrPath[this.CurrPathSize++] = this.NewArcPathEle( x, y, r, sa, ea, cc );
if (close) {
this.PathLineTo( arcStartX, arcStartY );
}
}
JsGraph.prototype.PathAppendPolygon = function( xArray, yArray, cont, close, size ) {
// note: polygon is transformed here to canvas coordinates!
// cont -> continue last path without move
var ctr = this.CurrTrans;
var otr = this.GetObjTrans();
ctr.ObjTransXY( otr, xArray[0], yArray[0] );
if (cont) {
this.PathLineTo( ctr.x, ctr.y );
} else {
this.PathMoveTo( ctr.x, ctr.y );
}
size = xDefNum( size, xArray.length );
for (var i = 1; i < size; i++ ) {
ctr.ObjTransXY( otr, xArray[i], yArray[i] );
this.PathLineTo( ctr.x, ctr.y );
}
if (close) {
if (xArray[0] != xArray[xArray.length-1] || yArray[0] != yArray[yArray.length-1]) {
ctr.ObjTransXY( otr, xArray[0], yArray[0] );
this.PathLineTo( ctr.x, ctr.y );
}
}
}
JsGraph.prototype.PathAppendBezierTo = function( cx1, cy1, cx2, cy2, ex, ey ) {
// note: points in canvas coordinates
// first point of bezier must be inserted with PathMoveTo or PathLineTo
this.CurrPath[this.CurrPathSize++] = this.NewBezierPathEle( cx1, cy1, cx2, cy2, ex, ey );
}
JsGraph.prototype.DriverPathPoly = new JsgPolygon( false, 'JsGraph.DriverPathPoly' );
JsGraph.prototype.DriverPathClipPoly = new JsgPolygon( false, 'JsGraph.DrverPathClipPoly' );
JsGraph.prototype.DriverPathClipPolyList = new JsgPolygonList( false, 'JsGraph.DriverPathClipPolyList' );
JsGraph.prototype.DriverDrawPath = function( close, areaMode ) {
// returns true if some path is put to context, false if all is clipped
var plen = this.CurrPathSize;
if (!this.GraphClipEnabled) {
return this.DriverDrawPathPart( 0, plen, true, close );
}
if (areaMode) {
var quadrant = this.GetPathClipQuadrant( 0, plen );
if (quadrant == 0) {
// full inside -> no clipping
return this.DriverDrawPathPart( 0, plen, true, close );
} else if (quadrant == 1) {
// full outside -> no drawing
return false;
}
// clip area path
var poly = this.DriverGetPathPoly( 0, 0, false );
var polyClipped = this.ClipPolygonArea( poly,
this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax,
this.DriverPathClipPoly
);
return this.DriverDrawPathPoly( polyClipped, true, false );
} else {
// clip and draw path as contour set
var last;
var from = 0;
var newPath = true;
while ( (last = this.DriverNextPathEnd(from+1)) > 0 ) {
if (last - from > 1) {
var closeLast = (last == plen && close);
var quadrant = this.GetPathClipQuadrant( from, last );
if (quadrant == 0) {
// full inside -> no clipping
if (this.DriverDrawPathPart( from, last, newPath, closeLast )) {
newPath = false;
}
} else if (quadrant == 2) {
// clip path contour part
var poly = this.DriverGetPathPoly( from, last, closeLast );
var polyListClipped = this.ClipPolygon( poly,
this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax, this.Context2D.lineWidth/2,
this.DriverPathClipPolyList
);
if (polyListClipped.Size > 0) {
for (var i = 0; i < polyListClipped.Size; i++) {
var polyClipped = polyListClipped.PolyList[i];
if (this.DriverDrawPathPoly( polyClipped, newPath, false )) {
newPath = false;
}
}
}
} // else full outside -> no drawing
}
from = last;
} // end while
return !newPath;
}
}
JsGraph.prototype.DriverGetPathPoly = function( from, to, closeLast ) {
// creates a polygon from all path elements between from until to
// if to = 0 then the whole path is fetched
// the last sub area is closed and the first point is added to the end if closeLast is true
// no duplicate points are addet
var poly = this.DriverPathPoly.Reset();
var p = this.CurrPath;
var plen = this.CurrPathSize;
var closeArea = false;
if (to == 0) {
from = 0;
to = plen;
closeArea = true;
}
var lastMoveIx = from;
for (var i = from; i < to; i++) {
var c = p[i];
var t = c.t;
if (t == 1) {
// lineTo element
poly.AddPoint( c.x, c.y );
} else if (t == 2) {
// moveTo:
if (i > from) {
if (p[i-1].t == 2) {
// last element is moveTo: replace last moveTo with current moveTo
poly.RemoveLastPoint();
} else {
// close last element
var cl = p[lastMoveIx];
poly.AddPoint( cl.x, cl.y );
}
}
poly.AddPoint( c.x, c.y );
lastMoveIx = i;
} else if (t == 0) {
// close path element
var c = p[lastMoveIx];
poly.AddPoint( c.x, c.y );
} else if (t == 3) {
// arc element
var startAng = this.RadToAngle(c.sa);
var endAng = this.RadToAngle(c.ea)
var rad = c.cc ? -c.r : c.r;
var ell = this.MakeEllipseArcPolygon( c.x, c.y, rad, c.r, 0, startAng, endAng );
poly.AddPoly( ell );
} else if (t == 4) {
// bezier element
var cprev = p[i-1];
var bezier = this.MakeBezierPolygon( cprev.x, cprev.y, c.cx1, c.cy1, c.cx2, c.cy2, c.ex, c.ey, this.NumBezierSegments );
poly.AddPoly( bezier );
}
}
// close last area part from end to last moveTo and then connect last with first point
if (closeArea || closeLast) {
var px = p[lastMoveIx].x;
var py = p[lastMoveIx].y;
var last = poly.Size - 1;
if (px != poly.X[last] || py != poly.Y[last] ) {
poly.AddPoint( px, py );
}
}
if (closeArea) {
var px = p[0].x;
var py = p[0].y;
var last = poly.Size - 1;
if (px != poly.X[last] || py != poly.Y[last] ) {
poly.AddPoint( px, py );
}
}
return poly;
}
JsGraph.prototype.DriverDrawPathPart = function( from, to, newPath, close ) {
// returns true if some elements are put to context
var p = this.CurrPath;
var ctx = this.Context2D;
if (newPath) ctx.beginPath();
for (var i = from; i < to; i++) {
var c = p[i];
var t = c.t;
if (t == 1) {
ctx.lineTo( c.x, c.y );
} else if (t == 2) {
ctx.moveTo( c.x, c.y );
} else if (t == 0) {
ctx.closePath();
} else if (t == 3) {
ctx.arc( c.x, c.y, c.r, c.sa, c.ea, c.cc );
} else if (t == 4) {
ctx.bezierCurveTo( c.cx1, c.cy1, c.cx2, c.cy2, c.ex, c.ey );
}
}
if (close) ctx.closePath();
return to > from;
}
JsGraph.prototype.DriverDrawPathPoly = function( poly, newPath, close ) {
// returns true if some elements are put to context
// note: a new path is ony if return is true and newPath is true
var size = poly.Size;
if (size < 2) return false;
var ctx = this.Context2D;
var xs = poly.X;
var ys = poly.Y;
if (newPath) ctx.beginPath();
ctx.moveTo( xs[0], ys[0] );
for (var i = 1; i < size; i++) {
ctx.lineTo( xs[i], ys[i] );
}
if (close) ctx.closePath();
return true;
}
JsGraph.prototype.DriverNextPathEnd = function( from ) {
// returns index of next path contour part starting at from
// returns this.CurrPathSize if no path end (moveTo element) is found after from
// returns -1 if from >= this.CurrPathSize
var plen = this.CurrPathSize;
if (from >= plen) return -1;
var p = this.CurrPath;
for (var i = from; i < plen; i++) {
var c = p[i];
var t = c.t;
if (t == 2) {
// moveTo marks begin of new contour
return i;
}
}
return plen;
}
JsGraph.prototype.GetPathClipQuadrant = function( from, to ) {
// returns clip quadrant of path part between from until to
// 0 -> full inside GraphClip inner range or crossing inner range and full inside outer range -> no clipping needet
// 1 -> full outside inner range -> invisibe, skip drawing
// 2 -> crossing inner and outer range -> clipping at GraphClip inner range needet
function minmax( x, y ) {
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
// speed optimization: quick check wether path is full inside inner clip range
if (this.DriverIsPathInsideRect( from, to, this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax )) {
return 0;
}
// find bounding rectangle
var xmin = this.GraphClipOuterXmax + 1000;
var xmax = this.GraphClipOuterXmin - 1000;
var ymin = this.GraphClipOuterYmax + 1000;
var ymax = this.GraphClipOuterYmin - 1000;
var p = this.CurrPath;
for (var i = from; i < to; i++) {
var c = p[i];
var t = c.t;
if (t == 1 || t == 2) {
minmax( c.x, c.y );
} else if (t == 3) {
minmax( c.x-c.r, c.y-c.r );
minmax( c.x+c.r, c.y+c.r );
} else if (t == 4) {
minmax( c.cx1, c.cy1 );
minmax( c.cx2, c.cy2 );
minmax( c.ex, c.ey );
}
}
// check clip quadrant of bounding rectangle
return this.GetRectClipQuadrant( xmin, xmax, ymin, ymax );
}
JsGraph.prototype.DriverIsPathInsideRect = function( from, to, xmin, xmax, ymin, ymax ) {
// returns true if path is complete inside rectangle xmin..ymax
var p = this.CurrPath;
for (var i = from; i < to; i++) {
var c = p[i];
var t = c.t;
if (t == 1 || t == 2) {
if (c.x < xmin || c.x > xmax || c.y < ymin || c.y > ymax) return false;
} else if (t == 3) {
var x = c.x - c.r;
var y = c.y - c.r;
if (x < xmin || x > xmax || y < ymin || y > ymax) return false;
var x = c.x + c.r;
var y = c.y + c.r;
if (x < xmin || x > xmax || y < ymin || y > ymax) return false;
} else if (t == 4) {
if (c.cx1 < xmin || c.cx1 > xmax || c.cy1 < ymin || c.cy1 > ymax) return false;
if (c.cx2 < xmin || c.cx2 > xmax || c.cy2 < ymin || c.cy2 > ymax) return false;
if (c.ex < xmin || c.ex > xmax || c.ey < ymin || c.ey > ymax) return false;
}
}
return true;
}
// drawing primitives
JsGraph.prototype.PenUp = function() {
this.PenDown = false;
}
JsGraph.prototype.MoveTo = function( x, y ) {
// or MoveTo( pt )
// x, y: real coordinates
// pt: JsgVect2
if (JsgVect2.Ok(x)) return this.MoveTo( x[0], x[1] );
this.LastX = x;
this.LastY = y;
if (this.IsPathOpen) {
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), x, y );
this.PathMoveTo( ctr.x, ctr.y );
}
this.PenDown = true;
return this;
}
JsGraph.prototype.LineTo = function( x, y ) {
// or LineTo( pt )
// x, y: real coordinates
// pt: JsgVect2
// if PenUp is called previously, this call is equivalent to MoveTo
if (JsgVect2.Ok(x)) return this.LineTo( x[0], x[1] );
var ctr = this.CurrTrans;
if (this.IsPathOpen) {
ctr.ObjTransXY( this.GetObjTrans(), x, y );
if (this.PenDown) {
this.PathLineTo( ctr.x, ctr.y );
} else {
this.PathMoveTo( ctr.x, ctr.y );
}
} else {
if (this.PenDown) {
this.WorkLineXArray[0] = this.LastX;
this.WorkLineXArray[1] = x;
this.WorkLineYArray[0] = this.LastY;
this.WorkLineYArray[1] = y;
this.DriverDrawPoly( this.WorkLineXArray, this.WorkLineYArray, 2, false, false );
}
}
this.PenDown = true;
this.LastX = x;
this.LastY = y;
return this;
}
JsGraph.prototype.WorkLineXArray = [ 0, 0 ];
JsGraph.prototype.WorkLineYArray = [ 0, 0 ];
JsGraph.prototype.Line = function( x1, y1, x2, y2, append ) {
// or Line( pt1, pt2, append )
// x1, y1, x2, y2: real coordinates
// pt1, pt2: JsgVect2
// append: bool; true -> append line to path and draw a line from last point in path to (x1,y1)
if (JsgVect2.Ok(x1)) return this.Line( x1[0], x1[1], y1[0], y1[1], x2 );
append = xDefBool( append, false );
if (this.IsPathOpen) {
var ctr = this.CurrTrans;
ctr.ObjTransXY2( this.GetObjTrans(), x1, y1, x2, y2 );
if (append) {
this.PathLineTo( ctr.x1, ctr.y1 );
} else {
this.PathMoveTo( ctr.x1, ctr.y1 );
}
this.PathLineTo( ctr.x2, ctr.y2 );
} else {
this.WorkLineXArray[0] = x1;
this.WorkLineXArray[1] = x2;
this.WorkLineYArray[0] = y1;
this.WorkLineYArray[1] = y2;
this.DriverDrawPoly( this.WorkLineXArray, this.WorkLineYArray, 2, false, false );
}
this.PenDown = true;
this.LastX = x2;
this.LastY = y2;
return this;
}
JsGraph.prototype.Arrow = function( x1, y1, x2, y2, variant, mode, sym1, sym2 ) {
// or Arrow( pt1, pt2, variant, mode )
// x1, y1, x2, y2: real coordinates
// pt1, pt2: JsgVect2
// variant: bitmask (default = 1): 1 -> symbol at end, 2 -> symbol at start, 4 -> hide line, 8 -> shorten line
// mode: bitmask (default = 3): 1 -> border, 2 -> fill, 4 -> not used, 8 -> append line to path
// if sym1 is defined then SetMarkerSymbol(sym1) is called for start and end symbol
// if sym2 is defined then SetMarkerSymbol(sym2) is called for end symbol
// draws a line with arror markers on one or both ends.
// Use SetMarkerAttr to set marker and line attributes
if (JsgVect2.Ok(x1)) return this.Arrow( x1[0], x1[1], y1[0], y1[1], x2, y2 );
variant = xDefNum( variant, 1 );
mode = xDefNum( mode, 1+2 );
var ctr = this.CurrTrans;
ctr.ObjTransXY2( this.GetObjTrans(), x1, y1, x2, y2 );
// if path open and mode = append, draw line from last point to startpoint of arrow
if (this.IsPathOpen && (mode & 8)) {
this.PathLineTo( ctr.x1, ctr.y1 );
}
if (x1 == x2 && y1 == y2) {
if (this.IsPathOpen) {
this.PathMoveTo( ctr.x2, ctr.y2 );
}
this.PenDown = true;
this.LastX = x2;
this.LastY = y2;
return this;
}
var otr = this.ObjTrans;
var cnvsX1 = ctr.x1, cnvsY1 = ctr.y1, cnvsX2 = ctr.x2, cnvsY2 = ctr.y2;
otr.TransXY2( x1, y1, x2, y2 );
var x1orig = otr.x1, y1orig = otr.y1, x2orig = otr.x2, y2orig = otr.y2;
var x1corr = otr.x1, y1corr = otr.y1, x2corr = otr.x2, y2corr = otr.y2;
var oldTransState = otr.Enable( false );
if ((variant & 8) && (variant & 1)) {
// shorten line end by 1/4 lineWidth
var v = JsgVect2.New( cnvsX2-cnvsX1, cnvsY2-cnvsY1 );
var vd = JsgVect2.Scale( JsgVect2.Norm( v ), -this.Context2D.lineWidth/2 );
var vs = JsgVect2.Add( v, vd );
if (JsgVect2.ScalarProd( vs, v ) <= 0) {
// hide line
variant |= 4;
}
x2corr = ctr.InvTransX( vs[0] + cnvsX1 );
y2corr = ctr.InvTransY( vs[1] + cnvsY1 );
}
if ((variant & 8) && (variant & 2)) {
// shorten line start by 1/4 lineWidth
var v = JsgVect2.New( cnvsX1-cnvsX2, cnvsY1-cnvsY2 );
var vd = JsgVect2.Scale( JsgVect2.Norm( v ), -this.Context2D.lineWidth/2 );
var vs = JsgVect2.Add( v, vd );
if (JsgVect2.ScalarProd( vs, v ) <= 0) {
// hide line
variant |= 4;
}
var x1corr = ctr.InvTransX( vs[0] + cnvsX2 );
var y1corr = ctr.InvTransY( vs[1] + cnvsY2 );
}
if (!(variant & 4)) {
// only draw line if it is after shortening not zero length and not inversed
var drawit = true;
if (variant & 8) {
var v1 = JsgVect2.New( x2orig-x1orig, y2orig-y1orig );
var v2 = JsgVect2.New( x2corr-x1corr, y2corr-y1corr );
drawit = (JsgVect2.ScalarProd( v1, v2 ) > 0);
}
if (drawit) {
this.Line( x1corr, y1corr, x2corr, y2corr );
}
}
if (xStr(sym1)) this.SetMarkerSymbol( sym1 );
if (variant & 2) {
var mat = JsgMat2.RotatingToXY( cnvsX1-cnvsX2, cnvsY1-cnvsY2 );
this.Marker( x1corr, y1corr, mode&3, mat );
}
if (xStr(sym2)) this.SetMarkerSymbol( sym2 );
if (variant & 1) {
var mat = JsgMat2.RotatingToXY( cnvsX2-cnvsX1, cnvsY2-cnvsY1 );
this.Marker( x2corr, y2corr, mode&3, mat );
}
otr.Enable( oldTransState );
if (this.IsPathOpen) {
this.PathMoveTo( cnvsX2, cnvsY2 );
}
this.PenDown = true;
this.LastX = x2;
this.LastY = y2;
return this;
}
JsGraph.prototype.PolygonArrow = function( xArray, yArray, variant, lineMode, arrowMode, size, sym1, sym2 ) {
// or PolygonArrow( poly, variant, lineMode, arrowMode )
//
// variant: bitmask (default = 1): 1 -> symbol at end, 2 -> symbol at start, 4 -> hide line, 8 -> shorten line
// lineMode: bitmask (default = 1): 1 -> border, 2 -> fill, 4 -> close polygon, 8 -> append line to path
// arrowMode: bitmask (default = 3): 1 -> border, 2 -> fill
// size -> specifies number of segments to draw from xArray and yArray; defaults to xArray.length
// if sym1 is defined then SetMarkerSymbol(sym1) is called for start and end symbol
// if sym2 is defined then SetMarkerSymbol(sym2) is called for end symbol
// see Arrow() for description of arguments
if (JsgPolygon.Ok(xArray)) return this.PolygonArrow( xArray.X, xArray.Y, yArray, variant, lineMode, xArray.Size );
variant = xDefNum( variant, 1 );
lineMode = xDefNum( lineMode, 1 );
arrowMode = xDefNum( arrowMode, 1+2 );
size = xDefNum( size, xArray.length );
if (size < 1) return this;
if (size == 1) return this.Line( xArray[0], yArray[0], xArray[0], yArray[0], ((lineMode & 8) > 0) );
if (size == 2) return this.Arrow( xArray[0], yArray[0], xArray[1], yArray[1], variant, arrowMode );
// assert size > 2
var last = size-1;
// append polygon to path
if (this.IsPathOpen && (lineMode & 8)) {
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), xArray[0], yArray[0] );
this.PathLineTo( ctr.x, ctr.y );
}
if (!(variant & 4)) {
// polygon is not hidden
if (lineMode & 4) {
// polygon is closed -> use normal polygon function; skip appending
this.Polygon( xArray, yArray, lineMode & ~8, size );
} else {
// polygon is not closed -> draw polygon with shortened ends where an arrow is drawn, except in paths
var skip = ((variant & 1) && (variant & 2) && (size == 3));
if (!skip) {
if (this.IsPathOpen) {
// do not close or append polygon here -> lineMode&3
this.Polygon( xArray, yArray, lineMode&3, size );
} else {
// polygon is big enough to draw parts of it
var x1 = xArray[0];
var y1 = yArray[0];
var x2 = xArray[last];
var y2 = yArray[last];
if (variant & 2) {
xArray[0] = xArray[1];
yArray[0] = yArray[1];
}
if (variant & 1) {
xArray[last] = xArray[last-1]
yArray[last] = yArray[last-1]
}
// do not close or append polygon here -> lineMode&3
this.Polygon( xArray, yArray, lineMode&3, size );
xArray[0] = x1;
yArray[0] = y1;
xArray[last] = x2;
yArray[last] = y2;
}
}
}
}
// draw first and/or last segment of polygon as arrows
var hideSeg = this.IsPathOpen ? 4 : 0;
if (variant & 2) {
var x1 = xArray[0], y1 = yArray[0];
var x2 = xArray[1], y2 = yArray[1];
this.Arrow( x1, y1, x2, y2, (variant&8)+hideSeg+2, arrowMode&(1+2), sym1 );
}
if (variant & 1) {
var x1 = xArray[last-1], y1 = yArray[last-1];
var x2 = xArray[last], y2 = yArray[last];
this.Arrow( x1, y1, x2, y2, (variant&8)+hideSeg+1, arrowMode&(1+2), sym2 );
}
var i = (lineMode & 4) ? 0 : last;
this.PenDown = true;
this.LastX = xArray[i];
this.LastY = yArray[i];
// move cursor of path to last position
if (this.IsPathOpen) {
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), this.LastX, this.LastY );
this.PathMoveTo( ctr.x, ctr.y );
}
return this;
}
JsGraph.prototype.RectWH = function( x, y, w, h, mode, roll ) {
// or RectWH( JsgRect, mode )
// x, y: real coordinates of edge with least or most negativ values
// w, h: real > 0 width and height
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> inverse drawing direction for holes in paths
// mode & 8 -> continue path
if (JsgRect.Ok(x)) return this.Rect( x.x, x.y, x.x+x.w, x.y+x.h, y, w );
return this.Rect( x, y, x+w, y+h, mode, roll );
}
JsGraph.prototype.Rect = function( x1, y1, x2, y2, mode, roll ) {
// or Rect( pt1, pt2, mode, roll )
// or Rect( { xmin, ymin, xmax, ymax }, mode, roll )
// or Rect( JsgRect, mode, roll )
// or Rect() -> Rect( GetFrame(), 1 )
// x1, y1, x2, y2: real coordinates of any two opposite edges
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> inverse drawing direction for holes
// mode & 8 -> continue path
if (!xDef(x1)) {
// Rect( x1 = GetFrame(): JsgRect, y1 = mode = 1 )
var oldTransState = this.ObjTrans.Enable( false );
this.Rect( this.GetFrame(), 1 )
this.ObjTrans.Endable( oldTransState );
return this;
}
if (xObj(x1)) {
if (JsgRect.Ok(x1)) return this.Rect( x1.x, x1.y, x1.x+x1.w, x1.y+x1.h, y1, x2 );
return this.Rect( x1.xmin, x1.ymin, x1.xmax, x1.ymax, y1, x2 );
}
if (JsgVect2.Ok(x1)) return this.Rect( x1[0], x1[1], y1[0], y1[1], x2, y2 );
this.DriverDrawRect( x1, y1, x2, y2, mode, roll );
return this;
}
JsGraph.prototype.DriverDrawRect = function( x1, y1, x2, y2, mode, roll ) {
// x1, y1, x2, y2: real coordinates of any two opposite edges
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> inverse drawing direction for holes
// mode & 8 -> continue path
mode = xDefNum( mode, 1 );
roll = xDefNum( roll, 0 );
var ctr = this.CurrTrans;
var otr = this.ObjTrans;
var inv = !!(mode & 4);
if (this.IsPathOpen) {
// add rect as polygon to path
var poly = this.MakeRectPolygon( x1, y1, x2, y2, inv, roll );
this.Polygon( poly, mode & 11 );
return;
}
if (otr.Enabled && !otr.IsMoveOnly) {
// if object transformation is not move-only then draw rect as polygon
this.RectAsPolygon( x1, y1, x2, y2, mode, inv, roll );
return;
}
// ObjTrans is move only, unity trans or disabled, and path is not open
// draw rect with native functions
ctr.ObjTransXY2( this.GetObjTrans(), x1, y1, x2, y2 );
if (ctr.x1 > ctr.x2) { var tmp = ctr.x1; ctr.x1 = ctr.x2; ctr.x2 = tmp; }
if (ctr.y1 > ctr.y2) { var tmp = ctr.y1; ctr.y1 = ctr.y2; ctr.y2 = tmp; }
var ctx = this.Context2D;
var quadrant = 0; // init rectangle full inside clipping range
if (this.GraphClipEnabled) {
quadrant = this.GetRectClipQuadrant( ctr.x1, ctr.x2, ctr.y1, ctr.y2 );
}
if (quadrant == 0) {
// rectangle full inside clipping range, draw it using native rectangle function
if (mode & 2) {
ctx.fillRect( ctr.x1, ctr.y1, ctr.x2-ctr.x1, ctr.y2-ctr.y1 );
}
if (mode & 1) {
var oldJoin = ctx.lineJoin;
var oldCap = ctx.lineCap;
if (oldJoin == 'round') {
ctx.lineCap = 'round';
} else {
ctx.lineJoin = 'miter';
ctx.lineCap = 'square';
}
ctx.strokeRect( ctr.x1, ctr.y1, ctr.x2-ctr.x1, ctr.y2-ctr.y1 );
ctx.lineJoin = oldJoin;
ctx.lineCap = oldCap;
}
} else if (quadrant == 2) {
// rectangle needs clipping, convert it to polygon and draw and clip rectangle
this.RectAsPolygon( x1, y1, x2, y2, mode, inv, roll );
} // else rectangle full outside clipping range, don't draw it
}
JsGraph.prototype.RectAsPolygon = function( x1, y1, x2, y2, mode, inv, roll ) {
// x1, y1, x2, y2: Number; p1, p2: JsgVect2; coordinates of any two opposite edges
// roll: Integer: rolls point of rectangle down (n > 0) or up (n < 0) n steps.
// n = 1 rotates start/endpoint clockwise one step, lower left point becomes lower right point.
// inv = true -> points of polygon are inversed
var poly = this.MakeRectPolygon( x1, y1, x2, y2, inv, roll );
var oldJoin = this.LineJoin;
var oldCap = this.LineCap;
if (oldJoin == 'round') {
this.SetLineCap( 'round' );
} else {
this.SetLineJoin( 'miter' );
this.SetLineCap( 'square' );
}
this.Polygon( poly, mode & 11 );
this.SetLineJoin( oldJoin );
this.SetLineCap( oldCap );
}
JsGraph.prototype.MakeRectPolygon = function( x1, y1, x2, y2, inverse, roll ) {
// or MakeRectPolygon( p1, p2, inverse, roll )
// or MakeRectPolygon( { xmin, ymin, xmax, ymax }, clockWise, roll )
// or MakeRectPolygon( JsgRect, inverse, roll )
// Returns this.WorkPoly: JsgPolygon
//
// Returns a polygon consisting of 5 edge points of a rectangle in current coordinate system.
// x1, y1, x2, y2: Number; p1, p2: JsgVect2; coordinates of any two opposite edges
// roll: Integer: rolls point of rectangle down (n > 0) or up (n < 0) n steps.
// n = 1 rotates start/endpoint clockwise one step, lower left point becomes lower right point.
// inverse = true -> points of polygon are inversed
if (JsgVect2.Ok(x1)) return this.MakeRectPolygon( x1[0], x1[1], y1[0], y1[1], x2, y2 );
if (xObj(x1)) {
if (JsgRect.Ok(x1)) return this.MakeRectPolygon( x1.x, x1.y, x1.x+x1.w, x1.y+x1.h, y1, x2 );
return this.MakeRectPolygon( x1.xmin, x1.ymin, x1.xmax, x1.ymax, y1, x2 );
}
inverse = xDefBool( inverse, false );
roll = xDefNum( roll, 0 );
if (x1 > x2) { var tmp = x1; x1 = x2; x2 = tmp; }
if (y1 > y2) { var tmp = y1; y1 = y2; y2 = tmp; }
var poly = this.WorkPoly.Reset();
poly.AddPoint( x1, y1 );
poly.AddPoint( x2, y1 );
poly.AddPoint( x2, y2 );
poly.AddPoint( x1, y2 );
// rotate polygon
if (roll !== 0) poly.Roll( roll );
// close polygon
poly.AddPoint( poly.X[0], poly.Y[0] );
// inverse polygon
if (inverse) poly.Invert( );
return poly;
}
JsGraph.prototype.DegToRad = function( a ) {
return a / 180 * Math.PI;
}
JsGraph.prototype.RadToDeg = function( a ) {
return a / Math.PI * 180;
}
JsGraph.prototype.AngleToRad = function( a ) {
return this.AngleMeasure == 'deg' ? this.DegToRad(a) : a;
}
JsGraph.prototype.RadToAngle = function( a ) {
// returns a if AngleMeasure is 'rad', else a is converted into degrees
return this.AngleMeasure == 'deg' ? this.RadToDeg(a) : a;
}
JsGraph.prototype.AngleOfVector = function( x, y ) {
// or AngleOfVector( pt )
if (JsgVect2.Ok(x)) return this.AngleOfVector( x[0], x[1] );
var r = Math.sqrt( x*x + y*y );
var ang = 0;
if (r > 0) ang = Math.acos( x / r );
if (y < 0) ang = 2 * Math.PI - ang;
if (this.AngleMeasure == 'deg') ang = this.RadToDeg(ang);
return ang;
}
JsGraph.prototype.MakeArcFromPoints = function( x1, y1, x2, y2, r, big ) {
// returns { x, y, r, start, end }
var arc = { x:x1, y:y1, r:r, start:0, end:0 };
var absr = Math.abs(r);
var mx = (x2 - x1) / 2;
var my = (y2 - y1) / 2;
var ml = Math.sqrt( mx*mx + my*my );
if (ml == 0) return arc;
var hl = 0;
if (absr > ml) hl = Math.sqrt( absr*absr - ml*ml );
var hx = - hl * my / ml;
var hy = hl * mx / ml;
if ((r < 0) ^ big) {
hx = -hx;
hy = -hy;
}
arc.x = x1 + mx + hx;
arc.y = y1 + my + hy;
arc.start = this.AngleOfVector( x1 - arc.x, y1 - arc.y );
arc.end = this.AngleOfVector( x2 - arc.x, y2 - arc.y );
return arc;
}
JsGraph.prototype.MakeEllipseArcPolygon = function( x, y, rx, ry, rot, start, end, rPixel ) {
// or MakeEllipseArcPolygon( pt, rx, ry, rot, start, end, rPixel )
// all coordinates in current coordinate system
// all angles in current angle measure
// rPixel (optional) as the greatest radius in canvas coordinates. If not defined, it will be calculated here.
// returns this.WorkPoly: JsgPolygon
if (JsgVect2.Ok(x)) return this.MakeEllipseArcPolygon( x[0], x[1], y, rx, ry, rot, start, end );
ry = xDefNum( ry, Math.abs(rx) );
rot = xDefNum( rot, 0 );
start = xDefNum( start, 0 );
end = xDefNum( end, start+this.RadToAngle(2*Math.PI) );
var abs = Math.abs, max = Math.max;
var ctr = this.CurrTrans;
var otr = this.ObjTrans;
// compute gratest pixel radius rPixel
var absRx = abs( rx );
var absRy = abs( ry );
if (!xDef(rPixel)) {
var maxR = max( absRx, absRy );
var s = otr.MaxScaling();
var cnvsRx = abs( s * ctr.ScaleX * maxR );
var cnvsRy = abs( s * ctr.ScaleY * maxR );
rPixel = max( cnvsRx, cnvsRy );
}
rot = this.AngleToRad( rot );
start = this.AngleToRad( start );
end = this.AngleToRad( end );
// compute delta angle
var delta = this.CompDeltaAngle( rPixel, this.CurvePrecision / this.DevicePixelRatio );
// create unity circle polygon
var angles = { delta: delta, start: start, end: end };
if (rx < 0) angles.delta *= -1;
this.NormalizeAngles( angles );
rot = this.NormalizeAngle( rot );
var poly = this.MakeUnityArcPolygon( angles );
// transform unity arc to ellipse arc
var mat = JsgMat2.Transformation( absRx, absRy, rot, x, y );
JsgMat2.TransPolyXY( mat, poly.X, poly.Y, poly.Size );
return poly;
}
JsGraph.prototype.Circle = function( x, y, r, mode, startAngle ) {
// or Circle( pt, r, mode, startAngle )
// r < 0 -> clockwise, r > 0 -> counterclockwise
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// (mode & 4 -> close circle) ignored, circles are closed anyway
// mode & 8 -> continue path
if (JsgVect2.Ok(x)) return this.Circle( x[0], x[1], y, r, mode );
startAngle = xDefNum( startAngle, 0 );
var start = startAngle;
var end = startAngle + this.RadToAngle(2*Math.PI);
if (r < 0) {
start = end;
end = startAngle;
}
this.Arc( x, y, r, start, end, mode );
return this;
}
JsGraph.prototype.Arc = function( x, y, r, start, end, mode ) {
// or Arc( pt, r, start, end, mode )
// x, y: real coordinates
// r: real radius
// r < 0 -> clockwise, r > 0 -> counterclockwise
// start, end: real angles in radians or degress (see AngleMeasure)
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close arc by drawing a line from last to first point of arc
// mode & 8 -> continue path
if (JsgVect2.Ok(x)) return this.Arc( x[0], x[1], y, r, start, end );
var ctr = this.CurrTrans;
var cnvsRX = Math.abs( ctr.ScaleX * r );
var cnvsRY = Math.abs( ctr.ScaleY * r );
var cnvsRDiff = Math.abs( cnvsRX - cnvsRY );
if (this.DisableNativeArc || !this.ObjTrans.IsMoveOnly || cnvsRDiff > this.CurvePrecision/this.DevicePixelRatio) {
// if transformed arc is distorted to elliptic arc, draw as EllipseArc
this.EllipseArcAsPolygon( x, y, r, Math.abs(r), 0, start, end, mode );
} else {
this.DriverDrawArc( x, y, r, start, end, mode );
}
return this;
}
JsGraph.prototype.ArcTo = function( x, y, r, big, mode ) {
// or ArcTo( pt, r, big, mode )
// x, y: real endpoint
// r: real radius
// r < 0 -> clockwise, r > 0 -> counterclockwise
// big: boolean: chose short or long arc
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close polygon
if (JsgVect2.Ok(x)) return this.ArcTo( x[0], x[1], y, r, big );
this.ArcPt( this.LastX, this.LastY, x, y, r, big, mode|8 );
return this;
}
JsGraph.prototype.ArcPt = function( x1, y1, x2, y2, r, big, mode ) {
// or ArcPt( pt1, pt2, r, big, mode )
// x1, y1, x2, y2: real startpoint and endpoint
// r: real radius
// r < 0 -> clockwise, r > 0 -> counterclockwise
// big: boolean: chose short or long arc
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close polygon
// mode & 8 -> continue path
if (JsgVect2.Ok(x1)) return this.ArcPt( x1[0], x1[1], y1[0], y1[1], x2, y2, r );
big = xDefBool( big, false );
mode = xDefNum( mode, 1 );
var arc = this.MakeArcFromPoints( x1, y1, x2, y2, r, big );
this.Arc( arc.x, arc.y, arc.r, arc.start, arc.end, mode );
this.PenDown = true;
this.LastX = x2;
this.LastY = y2;
return this;
}
JsGraph.prototype.DriverDrawArc = function( x, y, r, start, end, mode ) {
// Draws arc with native Context2D function if no clipping is required.
// Requires in that case that a perfect circular arc after transformation
// If cannot draw native, arc is drawn using EllipseArcAsPolygon.
// x, y: real coordinates in active coordinate system.
// r: real radius
// r < 0 -> inverse
// start, end: real angles in radians or degress (see AngleMeasure)
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close arc by drawing a line from last to first point of arc
// mode & 8 -> continue path
// require( this.ObjTrans.IsMoveOnly )
start = xDefNum( start, 0 );
end = xDefNum( end, start+this.RadToAngle(2*Math.PI) );
mode = xDefNum( mode, 1 );
var cnvsStart = this.AngleToRad(start);
var cnvsEnd = this.AngleToRad(end);
var ctx = this.Context2D;
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), x, y );
var cnvsRX = Math.abs( ctr.ScaleX * r );
var angles = { delta: r, start: cnvsStart, end: cnvsEnd };
if (ctr.ScaleX * ctr.ScaleY < 0) {
angles.delta *= -1;
angles.start *= -1;
angles.end *= -1;
}
this.NormalizeAngles( angles );
var inverse = (angles.delta < 0);
if (this.IsPathOpen) {
this.PathAppendArc( ctr.x, ctr.y, cnvsRX, angles.start, angles.end, inverse, ((mode&8) > 0), ((mode&4) > 0) );
} else {
var quadrant = 0; // init circle full inside
if (this.GraphClipEnabled) {
quadrant = this.GetCircleClipQuadrant( ctr.x, ctr.y, cnvsRX );
}
if (quadrant == 0) {
// circle full inside clipping range, draw it using native arc function
if (mode & 2) {
ctx.beginPath();
ctx.arc( ctr.x, ctr.y, cnvsRX, angles.start, angles.end, inverse );
ctx.fill();
}
if (mode & 1) {
ctx.beginPath();
ctx.arc( ctr.x, ctr.y, cnvsRX, angles.start, angles.end, inverse );
if (mode & 4) ctx.closePath();
ctx.stroke();
}
} else if (quadrant == 2) {
// circle needs clipping, convert it to polygon and draw and clip polygon
this.EllipseArcAsPolygon( x, y, r, Math.abs(r), 0, start, end, mode );
} // else circle full outside clipping range, don't draw it
}
var rAbs = Math.abs(r);
this.LastX = x + rAbs * Math.cos( end );
this.LastY = y + rAbs * Math.sin( end );
}
JsGraph.prototype.Ellipse = function( x, y, rx, ry, rot, mode, startAngle ) {
// or Ellipse( pt, rx, ry, rot, mode, startAngle )
// x, y: real center
// pt: JsgVect2
// rx, ry: real radius
// rx < 0 -> clockwise, rx > 0 -> counterclockwise
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// rot: real angle in radians or degrees (see AngleMeasure)
// mode & 4 -> close polygon
// mode & 8 -> continue path
if (JsgVect2.Ok(x)) return this.Ellipse( x[0], x[1], y, rx, ry, rot, mode );
startAngle = xDefNum( startAngle, 0 );
var start = startAngle;
var end = startAngle + this.RadToAngle(2*Math.PI);
if (rx < 0) {
start = end;
end = startAngle;
}
this.EllipseArc( x, y, rx, ry, rot, start, end, mode );
return this;
}
JsGraph.prototype.EllipseArc = function( x, y, rx, ry, rot, start, end, mode ) {
// or EllipseArc( pt, rx, ry, rot, start, end, mode )
// x, y: real center
// pt: JsgVect2
// rx, ry: real radius
// rx < 0 -> clockwise, rx > 0 -> counterclockwise
// start, end: real angles in ellipse coordinates as radians or degrees (see AngleMeasure)
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> force close polygon
// mode & 8 -> continue path
if (JsgVect2.Ok(x)) return this.EllipseArc( x[0], x[1], y, rx, ry, rot, start, end );
var ctr = this.CurrTrans;
var abs = Math.abs;
var precision = this.CurvePrecision/this.DevicePixelRatio;
var isCircular = !this.DisableNativeArc && this.ObjTrans.IsMoveOnly;
if (isCircular) {
// check wether rx in canvas coordinates is same in x and y direction
var cnvsRxx = abs( ctr.ScaleX * rx );
var cnvsRxy = abs( ctr.ScaleY * rx );
if (abs(cnvsRxx - cnvsRxy) > precision) isCircular = false;
if (isCircular) {
// check wether ry in canvas coordinates is same in x and y direction
var cnvsRyx = abs( ctr.ScaleX * ry );
var cnvsRyy = abs( ctr.ScaleY * ry );
if (abs(cnvsRyx - cnvsRyy) > precision) isCircular = false;
if (isCircular) {
// check wether rx and ry in canvas coordinates are equal within some precition
if (abs(cnvsRxx - cnvsRyx) > precision) isCircular = false;
}
}
}
if (isCircular) {
// allipse arc is a circular arc, so use native arc draw function
rot = xDefNum( rot, 0 );
start = xDefNum( start, 0 );
end = xDefNum( end, start+this.RadToAngle(2*Math.PI) );
this.DriverDrawArc( x, y, rx, start+rot, end+rot, mode );
} else {
this.EllipseArcAsPolygon( x, y, rx, ry, rot, start, end, mode );
}
return this;
}
JsGraph.prototype.IsClosedPolygon = function( xArray, yArray, size ) {
// or IsClosedPolygon( JsgPolygon )
// returns true if polygon startpoint and endpoints are closer than 1/2 pixels
if (JsgPolygon.Ok(xArray)) return this.IsClosedPolygon( xArray.X, xArray.Y, xArray.Size );
size = xDefNum( size, xArray.length );
var closed = false;
var last = size-1;
if (last >= 2) {
var refLen = 0.5 / this.DevicePixelRatio;
if (JsgVect2.Length2( xArray[0] - xArray[last], yArray[0] - yArray[last] ) <= (refLen*refLen)) closed = true;
}
return closed;
}
JsGraph.prototype.EllipseArcAsPolygon = function( x, y, rx, ry, rot, start, end, mode ) {
// draws ellipse arc as a polygon, no native driver function used
// x, y: real center in active coordinate system
// rx, ry: real radius in current coordinate system
// rx < 0 -> clockwise, rx > 0 -> counterclockwise
// start, end: real angles in ellipse coordinates as radians or degrees (see AngleMeasure)
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> force close polygon
// mode & 8 -> continue path
mode = xDefNum( mode, 1 );
var ell = this.MakeEllipseArcPolygon( x, y, rx, ry, rot, start, end );
var closed = this.IsClosedPolygon( ell.X, ell.Y, ell.Size );
if (closed) {
var ctx = this.Context2D
var oldJoin = ctx.lineJoin;
var oldCap = ctx.lineCap;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
this.Polygon( ell, mode );
ctx.lineJoin = oldJoin;
ctx.lineCap = oldCap;
} else {
this.Polygon( ell, mode );
}
}
JsGraph.prototype.DriverWorkPoly = new JsgPolygon( false, 'JsGraph.DriverWorkPoly' );
JsGraph.prototype.DriverWorkClipPoly = new JsgPolygon( false, 'JsGraph.DriverWorkClipPoly' );
JsGraph.prototype.DriverWorkClipPolyList = new JsgPolygonList( false, 'JsGraph.DriverWorkClipPolyList' );
JsGraph.prototype.DriverDrawPoly = function( xArray, yArray, size, fillMode, close, poly ) {
// transforms, clips and draws a polygon to the context2D
// returns this.DriverWorkPoly to reuse it in the next call with the same polygon to avoid multiple transformations
// first call für any polygon must omit poly
// require size > 0
var ctr = this.CurrTrans;
var otr = this.GetObjTrans();
var ctx = this.Context2D;
// make and transform polygon from parameters
if (!xDef(poly)) {
poly = this.DriverWorkPoly.Reset();
poly.Quadrant = -1;
for (var i = 0; i < size; i++) {
ctr.ObjTransXY( otr, xArray[i], yArray[i] );
poly.AddPoint( ctr.x, ctr.y );
}
} // else reuse poly
if (this.GraphClipEnabled) {
// handle clipping: first find out wether poly is visible and needs clipping
var quadrant = poly.Quadrant;
if (quadrant == -1) {
quadrant = this.GetPolygonClipQuadrant( poly.X, poly.Y, poly.Size );
poly.Quadrant = quadrant;
}
if (fillMode) {
// area clipping
var polyClipped = null;
if (quadrant == 0) {
// full inside, no clipping needet
polyClipped = poly;
} else if (quadrant == 2) {
// clip poly; poly needs to be closed for clipping to work properly
var didClosePoly = this.ClosePolygon( poly );
polyClipped = this.ClipPolygonArea( poly,
this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax,
this.DriverWorkClipPoly
);
if (didClosePoly) poly.RemoveLastPoint();
} // else full outside -> skip drawing
if (polyClipped) {
if (this.DriverDrawPathPoly( polyClipped, true, false )) {
ctx.fill();
}
}
} else {
// contour clipping
if (quadrant == 0) {
// full inside, no clipping needet
if (this.DriverDrawPathPoly( poly, true, close )) {
ctx.stroke();
}
} else if (quadrant == 2) {
// clip and draw poly
var didClosePoly = false;
if (close) didClosePoly = this.ClosePolygon( poly );
var polyListClipped = this.ClipPolygon( poly,
this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax, ctx.lineWidth/2,
this.DriverWorkClipPolyList
);
if (polyListClipped.Size > 0) {
var newPath = true;
for (var i = 0; i < polyListClipped.Size; i++) {
var polyClipped = polyListClipped.PolyList[i];
if (this.DriverDrawPathPoly( polyClipped, newPath, false )) {
newPath = false;
}
}
if (!newPath) ctx.stroke();
}
if (didClosePoly) poly.RemoveLastPoint();
} // else full outside -> skip drawing
}
} else {
// clipping disabled
if (this.DriverDrawPathPoly( poly, true, close )) {
if (fillMode) {
ctx.fill();
} else {
ctx.stroke();
}
}
}
return poly;
}
JsGraph.prototype.DriverDrawBezierCurve = function( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments ) {
// Draws bezier curve with native Context2D function if no clipping is required.
// If bezier needs clipping, bezier is drawn using BezierCurveAsPolygon.
// All coordinates in active coordinate system.
// sx/sy: startpoint; cx1/cy1/cx2/cy2: ontrol points; ex/ey: endpoint
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close curve by drawing a line from end point to start point
// mode & 8 -> continue path
mode = xDefNum( mode, 1 );
var ctx = this.Context2D;
var ctr = this.CurrTrans;
var otr = this.GetObjTrans();
ctr.ObjTransXY( otr, sx, sy );
var cnvsSx = ctr.x, cnvsSy = ctr.y;
ctr.ObjTransXY( otr, cx1, cy1 );
var cnvsCx1 = ctr.x, cnvsCy1 = ctr.y;
ctr.ObjTransXY( otr, cx2, cy2 );
var cnvsCx2 = ctr.x, cnvsCy2 = ctr.y;
ctr.ObjTransXY( otr, ex, ey );
var cnvsEx = ctr.x, cnvsEy = ctr.y;
if (this.IsPathOpen) {
if (mode & 8) {
this.PathLineTo( cnvsSx, cnvsSy );
} else {
this.PathMoveTo( cnvsSx, cnvsSy );
}
this.PathAppendBezierTo( cnvsCx1, cnvsCy1, cnvsCx2, cnvsCy2, cnvsEx, cnvsEy );
if (mode & 4) {
this.PathLineTo( cnvsSx, cnvsSy );
}
} else {
if (mode & 8) {
if (this.LastX != sx || this.LastY != sy ) {
this.Line( this.LastX, this.LastY, sx, sy );
}
}
var quadrant = 0; // init bezier curve full inside
if (this.GraphClipEnabled) {
quadrant = this.GetBezierClipQuadrant( cnvsSx, cnvsSy, cnvsCx1, cnvsCy1, cnvsCx2, cnvsCy2, cnvsEx, cnvsEy );
}
if (quadrant == 0) {
// bezier curve full inside clipping range, draw it using native bezier function
if (mode & 2) {
ctx.beginPath();
ctx.moveTo( cnvsSx, cnvsSy );
ctx.bezierCurveTo( cnvsCx1, cnvsCy1, cnvsCx2, cnvsCy2, cnvsEx, cnvsEy );
ctx.fill();
}
if (mode & 1) {
ctx.beginPath();
ctx.moveTo( cnvsSx, cnvsSy );
ctx.bezierCurveTo( cnvsCx1, cnvsCy1, cnvsCx2, cnvsCy2, cnvsEx, cnvsEy );
if (mode & 4) ctx.closePath();
ctx.stroke();
}
} else if (quadrant == 2) {
// bezier curve needs clipping, convert it to polygon and draw and clip polygon
this.BezierCurveAsPolygon( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments );
} // else bezier curve full outside clipping range, don't draw it
}
this.PenDown = true;
if (mode & 4) {
this.LastX = sx;
this.LastY = sy;
} else {
this.LastX = ex;
this.LastY = ey;
}
}
JsGraph.prototype.NewPoly = function() {
this.Poly.Reset();
return this;
}
JsGraph.prototype.CopyPoly = function( to, reuseArrays ) {
// returns a copy of this.Poly: { X: array(Size) of number, Y: array(Size) of number, Size: integer }
reuseArrays = xDefBool( reuseArrays, false );
return this.Poly.Copy( to, !reuseArrays );
}
JsGraph.prototype.AddPointToPoly = function( x, y ) {
// or AddPointToPoly( pt )
this.Poly.AddPoint( x, y );
return this;
}
JsGraph.prototype.AddVectToPoly = function( vec ) {
this.Poly.AddPoint( vec[0], vec[1] );
return this;
}
JsGraph.prototype.DrawPoly = function( mode ) {
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close polygon by drawing a line from last to first point of polygon
// mode & 8 -> continue path
// mode & 16 -> inverse Polygon
mode = xDefNum( mode, 1 );
if (mode & 16) this.Poly.Invert();
this.Polygon( this.Poly, mode );
}
JsGraph.prototype.DrawPolyArrow = function( variant, lineMode, arrowMode ) {
// variant: bitmask (default = 1): 1 -> symbol at end, 2 -> symbol at start, 4 -> hide line, 8 -> shorten line
// lineMode: bitmask (default = 1): 1 -> border, 2 -> fill, 4 -> close polygon, 8 -> append line to path
// arrowMode: bitmask (default = 3): 1 -> border, 2 -> fill
// size -> specifies number of segments to draw from xArray and yArray; defaults to xArray.length
// see Arrow() for description of arguments
this.PolygonArrow( this.Poly, variant, lineMode, arrowMode );
}
JsGraph.prototype.DrawPolyMarker = function( mode, mat ) {
// mode: int: 1 -> border, 2 -> fill, 3 -> fill and border
// mat: JsgMat2 (optional) -> additional transformation matrix (e.g. rotation)
// Use RotationMatrixToVect( x, y ) to create mat
this.Marker( this.Poly, mode, mat );
}
JsGraph.prototype.Polygon = function( xArray, yArray, mode, size ) {
// or Polygon( JsgPolygon, mode )
// xArray, yArray: array of real coordinates
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close polygon by drawing a line from last to first point of polygon
// mode & 8 -> continue path
// size -> specifies number of segments to draw from xArray and yArray; defaults to xArray.length
if (JsgPolygon.Ok(xArray)) return this.Polygon( xArray.X, xArray.Y, yArray, xArray.Size );
mode = xDefNum( mode, 1 );
size = xDefNum( size, xArray.length );
if (size < 1) return this;
if (this.IsPathOpen) {
this.PathAppendPolygon( xArray, yArray, ((mode&8) > 0), ((mode&4) > 0), size );
} else {
var poly;
if (mode & 2) {
// save transformed poly
poly = this.DriverDrawPoly( xArray, yArray, size, true, false );
}
if (mode & 1) {
// reuse transformed poly from previous call to DirverMakePath
this.DriverDrawPoly( xArray, yArray, size, false, (mode&4) > 0, poly );
}
}
var i = (mode & 4) ? 0 : xArray.length-1;
this.PenDown = true;
this.LastX = xArray[i];
this.LastY = yArray[i];
return this;
}
JsGraph.prototype.PolygonList = function( polyList, mode ) {
// or PolygonList( poly: JsgPolygon, mode )
// draws all polygons in polyList with function Polygon
if (!JsgPolygonList.Ok(polyList)) return g.Polygon( polyList, mode );
for (var i = 0; i < polyList.Size; i++) {
this.Polygon( polyList.PolyList[i], mode );
}
return this;
}
JsGraph.prototype.ClosePolygon = function( poly ) {
// returns true if poly has been closed, false if poly has already been closed
var last = poly.Size - 1;
if (last < 1) return false;
if (poly.X[0] != poly.X[last] || poly.Y[0] != poly.Y[last]) {
poly.AddPoint( poly.X[0], poly.Y[0] );
return true;
}
return false;
}
JsGraph.prototype.WorkPolyClipped = new JsgPolygon( false, 'JsGraph.WorkPolyClipped' );
JsGraph.prototype.ClipPolygonArea = function( poly, xmin, xmax, ymin, ymax, polyClipped ) {
// returns cliped poly in polyClipped
// if polyClipped is not defined, a new JsgPolygon is created and returned
// use GetPolygonQuadrant() or GetPolygonClipQuadrant() to check wether poly needs clipping or not
polyClipped = polyClipped || new JsgPolygon();
var polyClipped2 = this.WorkPolyClipped;
this.ClipPolygonAtLine( poly, xmin, false, false, polyClipped2 );
this.ClipPolygonAtLine( polyClipped2, ymin, false, true, polyClipped );
this.ClipPolygonAtLine( polyClipped, xmax, true, false, polyClipped2 );
this.ClipPolygonAtLine( polyClipped2, ymax, true, true, polyClipped );
return polyClipped;
}
JsGraph.prototype.WorkPolyListClipped = new JsgPolygonList( false, 'JsGraph.WorkPolyListClipped' );
JsGraph.prototype.ClipPolygon = function( poly, xmin, xmax, ymin, ymax, extend, polyListClipped ) {
// returns cliped poly in polyListClipped of type JsgPolgonList
// if polyListClipped is not defined, a new JsgPolygonList is created and returned
// use GetPolygonQuadrant() or GetPolygonClipQuadrant() to check wether poly needs clipping or not
extend = xDefNum( extend, 0 );
if (extend != 0) {
xmin -= extend;
xmax += extend;
ymin -= extend;
ymax += extend;
}
polyListClipped = polyListClipped || new JsgPolygonList();
var polyListClipped2 = this.WorkPolyListClipped;
polyListClipped2.Reset();
this.ClipPolygonAtLine( poly, xmin, false, false, polyListClipped2 );
polyListClipped.Reset();
for (var i = 0; i < polyListClipped2.Size; i++) {
this.ClipPolygonAtLine( polyListClipped2.PolyList[i], ymin, false, true, polyListClipped );
}
polyListClipped2.Reset();
for (var i = 0; i < polyListClipped.Size; i++) {
this.ClipPolygonAtLine( polyListClipped.PolyList[i], xmax, true, false, polyListClipped2 );
}
polyListClipped.Reset();
for (var i = 0; i < polyListClipped2.Size; i++) {
this.ClipPolygonAtLine( polyListClipped2.PolyList[i], ymax, true, true, polyListClipped );
}
return polyListClipped;
}
JsGraph.prototype.ClipPolygonAtLine = function( poly, clipCoord, clipAtMax, clipHorizontal, polyClipped ) {
// polyClipped: JsgPolygon or JsgPolygonList
function AddPointToPolyClipped( x, y ) {
if (clipHorizontal) {
polyClipped.AddPoint( y, x );
} else {
polyClipped.AddPoint( x, y );
}
}
function IsInside( x ) {
return clipAtMax ? x <= clipCoord : x >= clipCoord;
}
// return empty polygon if poly is empty
var isBorderClipMode = JsgPolygonList.Ok( polyClipped );
if (!isBorderClipMode) polyClipped.Reset();
if (poly.Size == 0) return;
// change coordinates if clipHorizontal is selected
var isP1Inside, isP2Inside, polyX, polyY;
if (clipHorizontal) {
polyX = poly.Y;
polyY = poly.X;
} else {
polyX = poly.X;
polyY = poly.Y;
}
// check if poly is only one point
isP1Inside = IsInside( polyX[0] );
if (poly.Size == 1) {
if (isP1Inside) {
if (isBorderClipMode) polyClipped.NewPoly();
polyClipped.AddPoint( poly.X[0], poly.Y[0] );
}
return;
}
// area in poly must be closed
var polyClosed = false;
if (!isBorderClipMode) {
polyClosed = this.ClosePolygon( poly );
}
// loop for all poly segments
var isLastP2Added = false;
var nlast = poly.Size - 2;
for (var i = 0; i <= nlast; i++) {
var i2 = i + 1;
isP2Inside = IsInside( polyX[i2] );
if (isP1Inside && isP2Inside) {
// both points inside: add segment to clipPoly
if (!isLastP2Added) {
if (isBorderClipMode) polyClipped.NewPoly();
AddPointToPolyClipped( polyX[i], polyY[i] );
}
AddPointToPolyClipped( polyX[i2], polyY[i2] );
isLastP2Added = true;
} else if (isP1Inside != isP2Inside) {
// segment intersects clipping line: handle clipping
var intersectCoord = this.ClipIntersectionCoord( polyX[i], polyY[i], polyX[i2], polyY[i2], clipCoord );
if (isP1Inside) {
// line segment exits inside
if (!isLastP2Added) {
if (isBorderClipMode) polyClipped.NewPoly();
AddPointToPolyClipped( polyX[i], polyY[i] );
}
AddPointToPolyClipped( clipCoord, intersectCoord );
isLastP2Added = false;
} else {
// line segment enters inside
if (isBorderClipMode) polyClipped.NewPoly();
AddPointToPolyClipped( clipCoord, intersectCoord );
AddPointToPolyClipped( polyX[i2], polyY[i2] );
isLastP2Added = true;
}
// } else { nothing to do if both points are outside
}
isP1Inside = isP2Inside;
} // next segment
if (polyClosed) poly.RemoveLastPoint();
}
JsGraph.prototype.GetRectQuadrant = function( rxmin, rxmax, rymin, rymax, xmin, xmax, ymin, ymax ) {
// rxmin..rymax : rect geometry
// xmin..ymax: clipping range
// returns: 0 -> rect full inside; 1 -> full outside; 2 -> crossing
if (rxmin >= xmin && rxmax <= xmax && rymin >= ymin && rymax <= ymax) return 0;
if (rxmax < xmin || rxmin > xmax || rymax < ymin || rymin > ymax) return 1;
return 2;
}
JsGraph.prototype.GetRectClipQuadrant = function( rxmin, rxmax, rymin, rymax ) {
// rxmin..rymax : rect geometry in canvas coordinates
// returns:
// 0 -> full inside GraphClip inner range or crossing inner range and full inside outer range -> no clipping needet
// 1 -> full outside inner range -> invisibe, skip drawing
// 2 -> crossing inner and outer range -> clipping at GraphClip inner range needet
// check full inside inner range
if (
rxmin >= this.GraphClipInnerXmin &&
rxmax <= this.GraphClipInnerXmax &&
rymin >= this.GraphClipInnerYmin &&
rymax <= this.GraphClipInnerYmax
) return 0;
// check full outside inner range
if (
rxmax < this.GraphClipInnerXmin ||
rxmin > this.GraphClipInnerXmax ||
rymax < this.GraphClipInnerYmin ||
rymin > this.GraphClipInnerYmax
) return 1;
// assert: rect cannot be full outside outer range here
// rect is crossing inner range, check crossing outer range
// check full inside outer range
if (
rxmin >= this.GraphClipOuterXmin &&
rxmax <= this.GraphClipOuterXmax &&
rymin >= this.GraphClipOuterYmin &&
rymax <= this.GraphClipOuterYmax
) return 0;
// rect cannot be full inside outer range not full outside outer range, so it must be crossing inner and outer range here
return 2;
}
JsGraph.prototype.GetPolygonClipQuadrant = function( xArray, yArray, size ) {
// 0 -> full inside GraphClip inner range or crossing inner range and full inside outer range -> no clipping needet
// 1 -> full outside inner range -> invisibe, skip drawing
// 2 -> crossing inner and outer range -> clipping at GraphClip inner range needet
// require size > 0
// optimization: quick check wether polygon is full inside inner range
if (this.IsPolygonArrayInsideRect( xArray, yArray, size, this.GraphClipInnerXmin, this.GraphClipInnerXmax, this.GraphClipInnerYmin, this.GraphClipInnerYmax )) {
return 0;
}
// find minimum and maximum coordinates
var xmin = xArray[0];
var xmax = xmin;
var ymin = yArray[0];
var ymax = ymin;
for (var i = 1; i < size; i++) {
var x = xArray[i];
var y = yArray[i];
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
return this.GetRectClipQuadrant( xmin, xmax, ymin, ymax );
}
JsGraph.prototype.GetCircleClipQuadrant = function( x, y, r ) {
// returns: 0 -> circle full inside; 1 -> full outside; 2 -> crossing
return this.GetRectClipQuadrant( x-r, x+r, y-r, y+r );
}
JsGraph.prototype.GetBezierClipQuadrant = function( px1, py1, cx1, cy1, cx2, cy2, px2, py2 ) {
// returns: 0 -> bezier curve full inside; 1 -> full outside; 2 -> crossing
var rxmin = Math.min( px1, cx1, cx2, px2 );
var rxmax = Math.max( px1, cx1, cx2, px2 );
var rymin = Math.min( py1, cy1, cy2, py2 );
var rymax = Math.max( py1, cy1, cy2, py2 );
return this.GetRectClipQuadrant( rxmin, rxmax, rymin, rymax );
}
JsGraph.prototype.IsPointInsideRect = function( x, y, xmin, xmax, ymin, ymax ) {
return (x >= xmin && x <= xmax && y >= ymin && y <= ymax);
}
JsGraph.prototype.IsPolygonInsideRect = function( xArray, yArray, size, xmin, xmax, ymin, ymax ) {
// or IsPolygonInsideRect( JsgPolygon, xmin, xmax, ymin, ymax )
// returns true if poly is complete inside rectangle
if (JsgPolygon.Ok(xArray)) {
return this.IsPolygonArrayInsideRect( xArray.X, xArray.Y, xArray.Size, yArray, size, xmin, xmax );
} else {
return this.IsPolygonArrayInsideRect( xArray, yArray, size, xmin, xmax, ymin, ymax );
}
}
JsGraph.prototype.IsPolygonArrayInsideRect = function( xArray, yArray, size, xmin, xmax, ymin, ymax ) {
// returns true if poly is complete inside rectangle
for (var i = 0; i < size; i++) {
if (xArray[i] < xmin || xArray[i] > xmax || yArray[i] < ymin || yArray[i] > ymax) return false;
}
return true;
}
JsGraph.prototype.ClipIntersectionCoord = function( x1, y1, x2, y2, clipx ) {
return (y2 - y1) * (clipx - x1) / (x2 - x1) + y1;
}
JsGraph.prototype.BezierCurveTo = function( cx1, cy1, cx2, cy2, ex, ey, mode ) {
// or BezierCurveTo( cpt1, cpt2, ept, mode )
// or BezierCurveTo( JsgPolygon, mode )
// LastX/LastY: startpoint; cx1/cy1/cx2/cy2: control points; ex/ey: endpoint
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close curve by drawing a line from end point to start point
if (JsgVect2.Ok(cx1)) return this.BezierCurve( this.LastX, this.LastY, cx1[0], cx1[1], cy1[0], cy1[1], cx2[0], cx2[1], cy2 );
if (JsgPolygon.Ok(cx1)) return this.BezierCurve( this.LastX, this.LastY, sx.X[0], sx.Y[0], sx.X[1], sx.Y[1], sx.X[2], sx.Y[2], sy );
mode = xDefNum( mode, 1 ) | 8;
this.BezierCurve( this.LastX, this.LastY, cx1, cy1, cx2, cy2, ex, ey, mode );
return this;
}
JsGraph.prototype.BezierCurve = function( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments ) {
// or BezierCurve( spt, cpt1, cpt2, ept, mode, nSegments )
// or BezierCurve( JsgPolygon, mode, startIx, nSegments )
// sx/sy: startpoint; cx1/cy1/cx2/cy2: ontrol points; ex/ey: endpoint
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> close curve by drawing a line from end point to start point
// mode & 8 -> continue path
// if nSegments is defined then native bezier is replaced by BezierPolygon
if (JsgVect2.Ok(sx)) {
return this.BezierCurve( sx[0], sx[1], sy[0], sy[1], cx1[0], cx1[1], cy1[0], cy1[1], cx2, cx1 );
}
if (JsgPolygon.Ok(sx)) {
var i = xDefNum( cx1, 0 ); // i = startIx
return this.BezierCurve( sx.X[i], sx.Y[i], sx.X[i+1], sx.Y[i+1], sx.X[i+2], sx.Y[i+2], sx.X[i+3], sx.Y[i+3], sy, cy1 );
}
if (this.DisableNativeBezier || xNum(nSegments)) {
this.BezierCurveAsPolygon( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments );
} else {
this.DriverDrawBezierCurve( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments );
}
return this;
}
JsGraph.prototype.MakeBezierPolygon = function( sx, sy, cx1, cy1, cx2, cy2, ex, ey, nSegments, add, polyRet ) {
// or MakeBezierPolygon( poly, startIx, nSegments, add, polyRet )
// or MakeBezierPolygon( s, c1, c2, e, nSegments, add, polyRet )
// if add is true then bezier polygon is added to polyRet
// polyRet: JsgPolygon(2D) or undefined
// returns polyRet or this.WorkPoly2
if (JsgVect2.Ok(sx)) {
return this.MakeBezierPolygon(
sx[0], sx[1], sy[0], sy[1], cx1[0], cx1[1], cy1[0], cy1[1], cx2, cy2, ex
);
}
if (JsgPolygon.Ok(sx)) {
polyRet = polyRet || this.WorkPoly2;
var startIx = xDefNum( sy, 0 );
if (sx.Size < startIx+4) {
add = xDefBool( add, false );
if (!add) polyRet.Reset();
return polyRet;
}
var i = xDefNum( sy, 0 ); // startIx
return this.MakeBezierPolygon(
sx.X[i+0], sx.Y[i+0], sx.X[i+1], sx.Y[i+1], sx.X[i+2], sx.Y[i+2], sx.X[i+3], sx.Y[i+3], cx1, cy1, cx2
);
}
nSegments = xDefNum( nSegments, this.NumBezierSegments );
add = xDefBool( add, false );
var polyRet = polyRet || this.WorkPoly2;
if (!add) polyRet.Reset();
var dt = 1 / nSegments;
var tlast = 1 + dt / 2;
for (var t = 0; t < tlast; t += dt) {
var t2 = t * t;
var t3 = t * t2;
var mt = 1 - t;
var mt2 = mt * mt;
var mt3 = mt * mt2;
var x = sx * mt3 + cx1 * 3 * mt2 * t + cx2 * 3 * mt * t2 + ex * t3;
var y = sy * mt3 + cy1 * 3 * mt2 * t + cy2 * 3 * mt * t2 + ey * t3;
polyRet.AddPoint( x, y );
}
return polyRet;
}
JsGraph.prototype.MakeSplineCurve = function( xArray, yArray, tension, mode, size, nSegments, polyRet ) {
// or MakeSplineCurve( JsgPolygon, tension, mode, nSegments, polyRet )
//
// xArray, yArray: array of real coordinates
//
// tension: number; curve parameter usually between 0 and 1 (0.5 is a good value) but not restricted to this range
//
// mode: default = 0
// mode & 4 -> draw closed spline
// mode & 16 -> skip first segment if spline is not closed
// mode & 32 -> skip last segment if spline is not closed
// mode & 64 -> use endpoints as spline points, else use endpoints as bezier control points
//
// size -> specifies number of segments to draw from xArray and yArray; defaults to xArray.length
// nSegments: 0 or undefined -> this.NumBezierSegments
// polyRet: JsgPolygon or undefined
//
// returns polyRet or WorkPoly2, which contains the resulting spline polygon
if (JsgPolygon.Ok(xArray)) {
// MakeSplineCurve( JsgPolygon, tension, mode, polyRet )
return this.MakeSplineCurve( xArray.X, xArray.Y, yArray, tension, xArray.Size, mode );
}
// compute bezier points and control points -> this.WorkPoly
var bezierPoly = this.SplineCurve( xArray, yArray, tension, mode, size );
// add bezier segments
polyRet = polyRet || this.WorkPoly2;
polyRet.Reset();
first = 0;
last = poly.Size - 1;
// skip first / last bezier segment if they are spline control points
if (!(mode & 4) && (mode & 64)) {
if ((mode & 16) && (last-first > 3)) first += 3;
if ((mode & 32) && (last-first > 3)) last -= 3;
}
for (var i = first; i < last; i += 3) {
// MakeBezierPolygon( poly, startIx, nSegments, add, polyRet )
this.MakeBezierPolygon( bezierPoly, i, nSegments, true, polyRet );
}
return polyRet;
}
JsGraph.prototype.BezierCurveAsPolygon = function( sx, sy, cx1, cy1, cx2, cy2, ex, ey, mode, nSegments ) {
// private function
nSegments = xDefNum( nSegments, this.NumBezierSegments );
var poly = this.MakeBezierPolygon( sx, sy, cx1, cy1, cx2, cy2, ex, ey, nSegments );
this.Polygon( poly, mode );
}
JsGraph.prototype.ComputeBezierControlPoints = function( poly, tension, last ) {
// Computes Control Points for quadratic Bezier Curves defined in poly
//
// poly: CPolygon = { X: array of number, Y: array of number, Size: integer }
// poly format: [ P0, C0b, C1a, P1, C1b, C2a, P2, C2b, C3a, P3, ... ]
//
// Note: places for control points C<i>a and C<i>b must already exist in poly
// last: index of last pivot point (not poly index but original spline point index)
//
// source: http://scaledinnovation.com/analytics/splines/aboutSplines.html
function LengthFor( side1, side2 ) {
return Math.sqrt( side1 * side1 + side2 * side2 );
}
var fa, fb;
var px = poly.X;
var py = poly.Y;
for (var i = 1; i <= last; i++) {
var pivot = 3 * i;
var left = pivot - 3;
var right = pivot + 3;
var ca = pivot - 1;
var cb = pivot + 1;
var d01 = LengthFor( px[pivot] - px[left], py[pivot] - py[left] );
var d12 = LengthFor( px[right] - px[pivot], py[right] - py[pivot] );
var d = d01 + d12;
if (d > 0) {
fa = tension * d01 / d;
fb = tension * d12 / d;
} else {
// note: d01 and d12 are also 0, so we are save if we set fa = fb = 0
fa = 0;
fb = 0;
}
var w = px[right] - px[left];
var h = py[right] - py[left];
px[ca] = px[pivot] - fa * w;
py[ca] = py[pivot] - fa * h;
px[cb] = px[pivot] + fb * w;
py[cb] = py[pivot] + fb * h;
}
}
JsGraph.prototype.SplineCurve = function( xArray, yArray, tension, mode, size ) {
// or SplineCurve( JsgPolygon, tension, mode )
//
// xArray, yArray: array of real coordinates
//
// tension: number; curve parameter usually between 0 and 1 (0.5 is a good value) but not restricted to this range
//
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> draw closed spline
// mode & 8 -> continue path
// mode & 16 -> skip first segment if spline is not closed
// mode & 32 -> skip last segment if spline is not closed
// mode & 64 -> use endpoints as spline points, else use endpoints as bezier control points
//
// size -> specifies number of segments to draw from xArray and yArray; defaults to xArray.length
//
// returns WorkPoly, which contains all Bezier segments
if (JsgPolygon.Ok(xArray)) {
return this.SplineCurve( xArray.X, xArray.Y, yArray, tension, xArray.Size );
}
tension = xDefNum( tension, 0.5 );
size = xDefNum( size, xArray.length );
if (size < 2) return this;
if (size == 2) {
return this.Line( xArray[0], yArray[0], xArray[1], yArray[1], (mode & 8) > 0 );
}
// make intermediate polygon for points and controlpoints [ p1, cp12, cp21, p2, cp22, cp31, p3, ... ]
var poly = this.WorkPoly.Reset();
var first = 0;
var last = size - 1;
var nPoints = size;
var firstIsControlPoint = (!(mode & 4) && (mode & 16) && !(mode & 64) && (nPoints >= 3));
if (firstIsControlPoint) {
first++;
nPoints--;
}
var lastIsControlPoint = (!(mode & 4) && (mode & 32) && !(mode & 64) && (nPoints >= 3));
if (lastIsControlPoint) {
last--;
nPoints--;
}
poly.AddPoint( xArray[first], yArray[first] );
for (var i = first+1; i <= last; i++) {
poly.AddPoint( 0, 0 ); // placeholder for control point 1
poly.AddPoint( 0, 0 ); // placeholder for control point 2
poly.AddPoint( xArray[i], yArray[i] );
}
var finalPolySize = poly.Size;
if (mode & 4) {
// closed spline: replicate first two points
poly.AddPoint( 0, 0 );
poly.AddPoint( 0, 0 );
poly.AddPoint( xArray[0], yArray[0] );
poly.AddPoint( 0, 0 );
poly.AddPoint( 0, 0 );
poly.AddPoint( xArray[1], yArray[1] );
finalPolySize = poly.Size - 3;
} else {
// set first and last control point
poly.X[1] = xArray[0];
poly.Y[1] = yArray[0];
var last = poly.Size - 2;
poly.X[last] = xArray[size-1];
poly.Y[last] = yArray[size-1];
}
// compute control points
var last = (mode & 4) ? size : size - 2;
if (firstIsControlPoint) last--;
if (lastIsControlPoint) last--;
this.ComputeBezierControlPoints( poly, tension, last );
// closed spline: move control points of last extra segment to first segment and cutoff p1 from poly
if (mode & 4) {
var i = poly.Size - 3;
poly.X[1] = poly.X[i];
poly.Y[1] = poly.Y[i];
poly.Size = finalPolySize;
}
// return if no draw mode is given
if (!(mode & 3)) return this.WorkPoly;
// open path to concatenate bezier segments
var oldIsPathOpen = this.IsPathOpen;
if (!oldIsPathOpen) {
this.OpenPath();
}
// draw bezier segments
first = 0;
last = poly.Size - 1;
// skip first / last bezier segment from drawing if they are spline control points
if (!(mode & 4) && (mode & 64)) {
if ((mode & 16) && (last-first > 3)) first += 3;
if ((mode & 32) && (last-first > 3)) last -= 3;
}
// clear flags 4, 16, 32, 64 (close, skip first, skip last) -> keep flags 1+2+8 = 11
var closedBorder = ((mode & 5) == 5);
mode = mode & 11;
for (var i = first; i < last; i += 3) {
this.BezierCurve( poly, mode, i );
mode |= 8; // continue path
}
if (!oldIsPathOpen) {
if (closedBorder) {
var ctx = this.Context2D
var oldCap = ctx.lineCap;
ctx.lineCap = 'round';
this.Path( mode & 3 );
ctx.lineCap = oldCap;
} else {
this.Path( mode & 3 );
}
}
return this.WorkPoly;
}
JsGraph.prototype.GetTextSize = function( txt, w ) {
// Gets Text Size without padding
// w optional with of limiting text box
// returns WorkRect: JsgRect with box size in w and h; x = 0, y = 0
var box = this.WorkRect;
box.SetPos( 0, 0 );
if (this.TextCanvasRendering) {
this.GetCanvasTextSize( txt, box );
} else {
w = xDefNum( w, 0 );
this.HtmlTextHandler.GetTextSize( txt, w, box );
}
box.w /= Math.abs( this.CurrTrans.ScaleX );
box.h /= Math.abs( this.CurrTrans.ScaleY );
return box;
}
JsGraph.prototype.GetTextBox = function( txt, x, y, w ) {
// or GetTextBox( txt, pt, w )
// x, y: real text reference point; pt: JsgVect2
// w: optional with of limiting text box in current coordinates (ignored when TextRendering = 'canvas')
// returns WorkRect: JsgRect
//
// You can draw the computed text box with the function JsGraph:RectWH()
// Note: compared with GetTextSize the box may be corrected if it overlaps with canvas border.
// Note: ObjTrans is not applied to x and y!
if (!xDef(x)) return this.GetTextBox( txt, 0, 0 );
if (JsgVect2.Ok(x)) return this.GetTextBox( txt, x[0], x[1], y );
var box = this.WorkRect;
w = xDefNum( w, 0 );
var ctr = this.CurrTrans;
var cnvsX = ctr.TransX( x );
var cnvsY = ctr.TransY( y );
if (this.TextCanvasRendering) {
this.GetCanvasTextBox( txt, cnvsX, cnvsY, box );
} else {
this.HtmlTextHandler.GetTextBox( txt, cnvsX, cnvsY, w, box );
}
// trick: to get the least or most negativ edge for all trans, compute the edge over the box center:
var cx = ctr.InvTransX( box.x + (box.w / 2) );
var cy = ctr.InvTransY( box.y + (box.h / 2 ) );
box.w = box.w / Math.abs(ctr.ScaleX);
box.h = box.h / Math.abs(ctr.ScaleY);
box.x = cx - box.w / 2;
box.y = cy - box.h / 2;
return box;
}
JsGraph.prototype.Text = function( txt, x, y, WidthOrMode ) {
// or Text( txt, pt, WidthOrMode )
// x, y: real text reference point
// w: optional limiting text rectangle width in pixels
// mode: default = 0: (canvas text only)
// 0 -> fill using TextColor, 1 -> stroke using LineColor, 2 -> fill using BgColor, 3 -> fill and stroke (canvas text only)
// x is left, center or right coordinate, depending on horizontal align
// y is top, middle or bottom coordinate, depending on vertical align
if (JsgVect2.Ok(x)) return this.Text( txt, x[0], x[1], y );
WidthOrMode = xDefNum( WidthOrMode, 0 );
var ctr = this.CurrTrans;
ctr.ObjTransXY( this.GetObjTrans(), x, y );
if (this.GraphClipEnabled && !this.IsPointInsideRect( ctr.x, ctr.y, this.GraphClipOuterXmin, this.GraphClipOuterXmax, this.GraphClipOuterYmin, this.GraphClipOuterYmax )) {
// if text position is outside clipping range don't draw text
return this;
}
if (this.TextCanvasRendering) {
this.DrawCanvasText( txt, ctr.x, ctr.y, WidthOrMode );
} else {
this.HtmlTextHandler.DrawText( txt, ctr.x, ctr.y, WidthOrMode );
}
return this;
}
JsGraph.prototype.TextBox = function( txt, x, y, mode, roll, width ) {
// or Text( txt, pt, mode, roll, width )
// draws the textbox for txt at pos x,y taking into account object transformation, text alignment, rotation and padding
// x, y: text reference point
// mode: 1 -> draw border, 2 -> fill, 3 -> border and fill, default = 1
// mode & 4 -> inverse drawing direction for holes
// mode & 8 -> continue path
// width: optional limiting text rectangle width in pixels (only for TextRendering = 'html')
if (JsgVect2.Ok(x)) return this.TextBox( txt, x[0], x[1], y, mode, roll );
if (this.TextCanvasRendering) {
var objTrans = this.SaveTrans( true );
this.TransMove( -x, -y );
this.TransScale( this.CurrTrans.ScaleX, this.CurrTrans.ScaleY );
this.TransRotate( this.TextRotation );
this.TransScale( 1 / this.CurrTrans.ScaleX, 1 / this.CurrTrans.ScaleY );
this.TransMove( x, y );
this.AddTrans( objTrans );
this.Rect( this.GetTextBox( txt, x, y ), mode, roll );
this.RestoreTrans();
} else {
this.Rect( this.GetTextBox( txt, x, y, width ), mode, roll );
}
return this;
}
JsGraph.prototype.DrawCanvasText = function( txt, x, y, mode ) {
// private function
// x, y: in current coord system
this.SetCanvasFont();
var ctr = this.CurrTrans;
var otr = this.ObjTrans;
var ctx = this.Context2D;
var oldFillStyle = ctx.fillStyle;
if (mode == 0) {
// fill text with TextColor
mode = 2;
ctx.fillStyle = this.TextColor;
}
if (!otr.IsUnitTrans || this.TextRotation != 0) {
// handle object transformation
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.scale( this.ContextScale, this.ContextScale );
ctx.translate( x, y );
ctx.scale( ctr.ScaleX, ctr.ScaleY );
ctx.transform( otr.a00, otr.a10, otr.a01, otr.a11, 0, 0 );
ctx.scale( 1/ctr.ScaleX, 1/ctr.ScaleY );
ctx.rotate( this.AngleToRad( this.TextRotation ) );
x = y = 0;
}
var box = this.WorkRect;
this.GetCanvasTextSize( txt, box );
box.w += 2 * this.CanvasTextHPad;
box.h += 2 * this.CanvasTextVPad;
if (this.TextHAlign == 'left') x += box.w / 2;
if (this.TextHAlign == 'right') x -= box.w / 2;
if (this.TextVAlign == 'top') y += box.h / 2;
if (this.TextVAlign == 'bottom') y -= box.h / 2;
if (mode & 2) {
ctx.fillText( txt, x, y );
}
if (mode & 1) {
ctx.strokeText( txt, x, y );
}
if (!otr.IsUnitTrans || this.TextRotation != 0) {
// Reinitialize Transform to Unity + Scale( this.ContextScale )
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.scale( this.ContextScale, this.ContextScale );
}
ctx.fillStyle = oldFillStyle;
}
JsGraph.prototype.GetCanvasTextSize = function( txt, box ) {
// private function: modifies box.w and box.h but not box.x and box.h
// box: JsgRect
this.SetCanvasFont();
var data = this.Context2D.measureText( txt );
box.SetSize( data.width, this.CanvasFontSize );
}
JsGraph.prototype.GetCanvasTextBox = function( txt, x, y, box ) {
// private function: modifies box.w and box.h and corrects box.x and box.h with alignment and padding
// x, y: reference point in canvas coordinates
// box: JsgRect
// output (box.x, box.y) is top left corner in canvas coordinates
this.GetCanvasTextSize( txt, box );
box.SetPos( x, y);
box.w += 2 * this.CanvasTextHPad;
box.h += 2 * this.CanvasTextVPad;
var hAlign = this.TextHAlign;
if (hAlign == 'justify') hAlign = 'center';
var vAlign = this.TextVAlign;
if (hAlign == 'center') box.x -= box.w / 2;
if (hAlign == 'right' ) box.x -= box.w;
if (vAlign == 'middle' ) box.y -= box.h / 2;
if (vAlign == 'bottom' ) box.y -= box.h;
}
JsGraph.prototype.SetCanvasFont = function() {
// private function
if (!this.TextCanvasRendering || this.CTextCurrFontVers == this.CTextLastFontVers) return;
this.CTextLastFontVers = this.CTextCurrFontVers;
var ctx = this.Context2D;
var attr = '';
if (this.FontStyle == 'italic') attr += 'italic ';
if (this.FontWeight == 'bold') attr += 'bold ';
attr += this.CanvasFontSize + 'px ';
if (this.CanvasLineHeight > 0) attr += '/ ' + this.CanvasLineHeight + 'px ';
if (this.CanvasLineHeight == 0) attr += '/ 100% ';
attr += this.TextFont;
ctx.font = attr;
// always use this text align
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
}
// axes and grids
JsGraph.prototype.StartXright = function( x, dx, winBorderLeft ) {
if ( x < winBorderLeft ) {
// move x into window
x = Math.floor( winBorderLeft / dx ) * dx;
if ( x < winBorderLeft ) x += dx;
// assert( x >= winBorderLeft );
}
return x;
}
JsGraph.prototype.StartXleft = function( x, dx, winBorderRight ) {
if ( x > winBorderRight ) {
// move x into window
x = - Math.floor( -winBorderRight / dx ) * dx;
if ( x > winBorderRight ) x -= dx;
// assert( x <= winBorderRight );
}
return x;
}
JsGraph.prototype.StartYup = function( y, dy, winBorderBottom ) {
if ( y < winBorderBottom ) {
// move y into window
y = Math.floor( winBorderBottom / dy ) * dy;
if ( y < winBorderBottom ) y += dy;
// assert( y >= winBorderBottom );
}
return y;
}
JsGraph.prototype.StartYdown = function( y, dy, winBorderTop ) {
if ( y > winBorderTop ) {
// move y into window
y = - Math.floor( -winBorderTop / dy ) * dy;
if ( y > winBorderTop ) y -= dy;
// assert( y <= winBorderTop );
}
return y;
}
JsGraph.prototype.Frame = function( mode ) {
// draws frame fully inside window/viewport
mode = xDefNum( mode, 1 );
var oldTrans = this.SelectTrans('viewport');
var oldObjTransEnable = this.ObjTrans.Enable( false );
var oldLineJoin = this.LineJoin;
var lwh = this.Context2D.lineWidth / 2 - 0.5;
this.SetLineJoin( 'miter' );
this.Rect( lwh, lwh, this.VpInnerWidth-lwh, this.VpInnerHeight-lwh, mode );
this.SetLineJoin( oldLineJoin );
this.ObjTrans.Enable( oldObjTransEnable );
this.SelectTrans( oldTrans );
}
JsGraph.prototype.GetFrame = function() {
// returns limits of current view (canvas, viewport or window)
// { xmin, ymin, xmax, ymax }
var rect = this.GetFrameRect();
return { xmin: rect.x, ymin: rect.y, xmax: rect.x+rect.w, ymax: rect.y+rect.h };
}
JsGraph.prototype.GetFrameRect = function() {
var rect = this.GetTransRect();
if (this.Trans == 'viewport') {
rect.w = this.VpInnerWidth;
rect.h = this.VpInnerHeight;
}
return rect;
}
JsGraph.prototype.GetTransRect = function( aTrans ) {
// get geom of transformation aTrans or this.Trans
if (!xStr(aTrans) || !this.TransByName[aTrans]) aTrans = this.Trans;
var trans = this.TransByName[aTrans];
return new JsgRect( trans.Xmin, trans.Ymin, trans.Width, trans.Height );
}
JsGraph.prototype.GetCanvasRect = function() {
return new JsgRect( 0, 0, this.CanvasWidth, this.CanvasHeight );
}
JsGraph.prototype.GetViewportRect = function() {
// returns bounding box rect of current viewport in canvas coordinates (integers)
var xmin = Math.floor( this.VpXmin );
var ymin = Math.floor( this.VpYmin );
var xmax = Math.floor( this.VpXmin + this.VpWidth + 0.9999 );
var ymax = Math.floor( this.VpYmin + this.VpHeight + 0.9999 );
return new JsgRect( xmin, ymin, xmax-xmin, ymax-ymin );
}
JsGraph.prototype.GetViewportDeviceRect = function( box ) {
// returns bounding box rect of current vierport in device pixel coordinates
var xmin = Math.floor( this.VpXmin * this.DevicePixelRatio );
var ymin = Math.floor( this.VpYmin * this.DevicePixelRatio );
var xmax = Math.floor( (this.VpXmin + this.VpWidth) * this.DevicePixelRatio + 0.9999 );
var ymax = Math.floor( (this.VpYmin + this.VpHeight) * this.DevicePixelRatio + 0.9999 );
return new JsgRect( xmin, ymin, xmax-xmin, ymax-ymin );
}
JsGraph.prototype.Grid = function( xTic, yTic, skipZero, skipLimit ) {
this.GridX( xTic, skipZero, skipLimit );
this.GridY( yTic, skipZero, skipLimit );
}
JsGraph.prototype.GridX = function( dx, skipZero, skipLimit ) {
dx = xDefNum( dx, 1 );
if (dx <= 0) return;
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, false );
var ctr = this.CurrTrans;
var box = this.GetFrame();
var ctx = this.Context2D;
// if dx - lineWidth is smaller then 1 devicePixel, don't draw the grid
var deviceLineSpacing = (Math.abs(ctr.ScaleX * dx) - ctx.lineWidth) * this.DevicePixelRatio;
if (deviceLineSpacing < 1) return;
var cnvsYmin = ctr.TransY( box.ymin );
var cnvsYmax = ctr.TransY( box.ymax );
if (box.xmin > box.xmax) { var tmp = box.xmin; box.xmin = box.xmax; box.xmax = tmp; }
var epsX = 1.0 / Math.abs(ctr.ScaleX);
ctx.beginPath();
if (box.xmax >= 0) {
var x = this.StartXright( ((skipZero) ? dx : 0), dx, box.xmin );
var xEnd = box.xmax + epsX;
if (skipLimit) xEnd -= dx;
while (x <= xEnd) {
var cnvsX = ctr.TransX( x );
ctx.moveTo( cnvsX, cnvsYmin );
ctx.lineTo( cnvsX, cnvsYmax );
x += dx;
}
}
if (box.xmin <= 0) {
var x = this.StartXleft( -dx, dx, box.xmax );
var xEnd = box.xmin - epsX;
if (skipLimit) xEnd += dx;
while (x >= xEnd) {
var cnvsX = ctr.TransX( x );
ctx.moveTo( cnvsX, cnvsYmin );
ctx.lineTo( cnvsX, cnvsYmax );
x -= dx;
}
}
var oldCap = ctx.lineCap;
ctx.lineCap = 'butt';
ctx.stroke();
ctx.lineCap = oldCap;
}
JsGraph.prototype.GridY = function( dy, skipZero, skipLimit ) {
dy = xDefNum( dy, 1 );
if (dy <= 0) return;
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, false );
var ctr = this.CurrTrans;
var box = this.GetFrame();
var ctx = this.Context2D;
// if dy - lineWidth is smaller then 1 devicePixel, don't draw the grid
var deviceLineSpacing = (Math.abs(ctr.ScaleY * dy) - ctx.lineWidth) * this.DevicePixelRatio;
if (deviceLineSpacing < 1) return;
var cnvsXmin = ctr.TransX( box.xmin );
var cnvsXmax = ctr.TransX( box.xmax );
if (box.ymin > box.ymax) { var tmp = box.ymin; box.ymin = box.ymax; box.ymax = tmp; }
var epsY = 1.0 / Math.abs(ctr.ScaleY);
ctx.beginPath();
if (box.ymax >= 0) {
var y = this.StartYup( ((skipZero) ? dy : 0), dy, box.ymin );
var yEnd = box.ymax + epsY;
if (skipLimit) yEnd -= dy;
while (y <= yEnd) {
var cnvsY = ctr.TransY( y );
ctx.moveTo( cnvsXmin, cnvsY );
ctx.lineTo( cnvsXmax, cnvsY );
y += dy;
}
}
if (box.ymin <= 0) {
var y = this.StartYdown( -dy, dy, box.ymax );
var yEnd = box.ymin - epsY;
if (skipLimit) yEnd += dy;
while (y >= yEnd) {
var cnvsY = ctr.TransY( y );
ctx.moveTo( cnvsXmin, cnvsY );
ctx.lineTo( cnvsXmax, cnvsY );
y -= dy;
}
}
var oldCap = ctx.lineCap;
ctx.lineCap = 'butt';
ctx.stroke();
ctx.lineCap = oldCap;
}
JsGraph.prototype.Axes = function( xPos, yPos, ArrowSymbol, ArrowSize ) {
this.AxesX( yPos, ArrowSymbol, ArrowSize );
this.AxesY( xPos, ArrowSymbol, ArrowSize );
}
JsGraph.prototype.AxesX = function( yPos, ArrowSymbol, ArrowSize ) {
yPos = xDefNum( yPos, 0 );
ArrowSymbol = xDefStr( ArrowSymbol, '' );
ArrowSize = xDefNum( ArrowSize, 8 );
var box = this.GetFrame();
var xMin = box.xmin;
var xMax = box.xmax;
if (xMin > xMax) { var tmp = xMin; xMin = xMax; xMax = tmp; }
var yMin = box.ymin;
var yMax = box.ymax;
if (yMin > yMax) { var tmp = yMin; yMin = yMax; yMax = tmp; }
var ctx = this.Context2D;
var oldCap = ctx.lineCap;
var oldObjTransEnable = this.ObjTrans.Enable( false );
ctx.lineCap = 'butt';
if (yPos >= yMin && yPos <= yMax) {
if (ArrowSymbol != '') {
this.SetMarkerSymbol( ArrowSymbol );
this.SetMarkerSize( ArrowSize );
this.Arrow( xMin, yPos, xMax, yPos, 9 );
} else {
this.Line( xMin, yPos, xMax, yPos );
}
}
ctx.lineCap = oldCap;
this.ObjTrans.Enable( oldObjTransEnable );
}
JsGraph.prototype.AxesY = function( xPos, ArrowSymbol, ArrowSize ) {
xPos = xDefNum( xPos, 0 );
ArrowSymbol = xDefStr( ArrowSymbol, '' );
ArrowSize = xDefNum( ArrowSize, 8 );
var box = this.GetFrame();
var xMin = box.xmin;
var xMax = box.xmax;
if (xMin > xMax) { var tmp = xMin; xMin = xMax; xMax = tmp; }
var yMin = box.ymin;
var yMax = box.ymax;
if (yMin > yMax) { var tmp = yMin; yMin = yMax; yMax = tmp; }
var ctx = this.Context2D;
var oldCap = ctx.lineCap;
var oldObjTransEnable = this.ObjTrans.Enable( false );
ctx.lineCap = 'butt';
if (xPos >= xMin && xPos <= xMax) {
if (ArrowSymbol != '') {
this.SetMarkerSymbol( ArrowSymbol );
this.SetMarkerSize( ArrowSize );
this.Arrow( xPos, yMin, xPos, yMax, 9 );
} else {
this.Line( xPos, yMin, xPos, yMax );
}
}
ctx.lineCap = oldCap;
this.ObjTrans.Enable( oldObjTransEnable );
}
JsGraph.prototype.TicsX = function( yPos, dx, ticUp, ticDown, skipZero, skipLimit ) {
// yPos: real y coordinate of x axes
// dx: real distance between two tics on x axes
// ticUp, ticDown: real tics size in pixels
yPos = xDefNum( yPos, 0 );
dx = xDefNum( dx, 1 );
if (dx <= 0) return;
ticUp = xDefNum( ticUp, 3 );
ticDown = xDefNum( ticDown, ticUp );
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, false );
if (this.AutoScalePix) {
ticUp = this.ScalePix( ticUp, this.ScalePixInt );
ticDown = this.ScalePix( ticDown, this.ScalePixInt );
}
var ctr = this.CurrTrans;
var box = this.GetFrame();
var cnvsY = ctr.TransY( yPos );
var ctx = this.Context2D;
// if dx - lineWidth is smaller then 1 devicePixel, don't draw the tics
var deviceLineSpacing = (Math.abs(ctr.ScaleX * dx) - ctx.lineWidth) * this.DevicePixelRatio;
if (deviceLineSpacing < 1) return;
if (box.xmin > box.xmax) { var tmp = box.xmin; box.xmin = box.xmax; box.xmax = tmp; }
var epsX = 1.0 / Math.abs(ctr.ScaleX);
ctx.beginPath();
if ( box.xmax >= 0 ) {
var x = this.StartXright( ((skipZero) ? dx : 0), dx, box.xmin );
var xEnd = box.xmax + epsX;
if ( skipLimit ) xEnd -= dx;
while ( x <= xEnd ) {
var cnvsX = ctr.TransX( x );
ctx.moveTo( cnvsX, cnvsY-ticUp );
ctx.lineTo( cnvsX, cnvsY+ticDown );
x += dx;
}
}
if ( box.xmin <= 0 ) {
var x = this.StartXleft( -dx, dx, box.xmax );
var xEnd = box.xmin - epsX;
if ( skipLimit ) xEnd += dx;
while ( x >= xEnd ) {
var cnvsX = ctr.TransX( x );
ctx.moveTo( cnvsX, cnvsY-ticUp );
ctx.lineTo( cnvsX, cnvsY+ticDown );
x -= dx;
}
}
var oldCap = ctx.lineCap;
ctx.lineCap = 'butt';
ctx.stroke();
ctx.lineCap = oldCap;
}
JsGraph.prototype.TicsY = function( xPos, dy, ticRight, ticLeft, skipZero, skipLimit ) {
// xPos: real x coordinate of y axes
// dy: real distance between two tics on y axes
// ticLeft, ticRight: real tics size in pixels
xPos = xDefNum( xPos, 0 );
dy = xDefNum( dy, 1 );
if (dy <= 0) return;
ticRight = xDefNum( ticRight, 3 );
ticLeft = xDefNum( ticLeft, ticRight );
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, false );
if (this.AutoScalePix) {
ticRight = this.ScalePix( ticRight, this.ScalePixInt );
ticLeft = this.ScalePix( ticLeft, this.ScalePixInt );
}
var ctr = this.CurrTrans;
var box = this.GetFrame();
var cnvsX = ctr.TransX( xPos );
var ctx = this.Context2D;
// if dy - lineWidth is smaller then 1 devicePixel, don't draw the grid
var deviceLineSpacing = (Math.abs(ctr.ScaleY * dy) - ctx.lineWidth) * this.DevicePixelRatio;
if (deviceLineSpacing < 1) return;
if (box.ymin > box.ymax) { var tmp = box.ymin; box.ymin = box.ymax; box.ymax = tmp; }
var epsY = 1.0 / Math.abs(ctr.ScaleY);
ctx.beginPath();
if ( box.ymax >= 0 ) {
var y = this.StartYup( ((skipZero) ? dy : 0), dy, box.ymin );
var yEnd = box.ymax + epsY;
if ( skipLimit ) yEnd -= dy;
while ( y <= yEnd ) {
var cnvsY = ctr.TransY( y );
ctx.moveTo( cnvsX-ticLeft, cnvsY );
ctx.lineTo( cnvsX+ticRight, cnvsY );
y += dy;
}
}
if ( box.ymin <= 0 ) {
var y = this.StartYdown( -dy, dy, box.ymax );
var yEnd = box.ymin - epsY;
if ( skipLimit ) yEnd += dy;
while ( y >= yEnd ) {
var cnvsY = ctr.TransY( y );
ctx.moveTo( cnvsX-ticLeft, cnvsY );
ctx.lineTo( cnvsX+ticRight, cnvsY );
y -= dy;
}
}
var oldCap = ctx.lineCap;
ctx.lineCap = 'butt';
ctx.stroke();
ctx.lineCap = oldCap;
}
JsGraph.prototype.MakeLabel = function( value, scale, digits, unit ) {
var v = (value * scale).toFixed(digits);
if (!xStr(unit) || unit == '') return v;
if (unit.indexOf('(#)') < 0) return v + unit;
return unit.replace( /\(#\)/, v );
}
JsGraph.prototype.TicLabelsX = function( yPos, dx, yOff, scale, digits, skipZero, skipLimit, aUnit ) {
// yPos: real y coordinate of x axes
// dx: real distance between two tics on x axes
// yOff: real offset in pixels: > 0 for label yOff above yPos, < 0 for under yPos
// labels can be scaled by a factor of scale; use 1 if no scaling is required
// digits: int [0..20]. specifies number of digits after decimal point
// set TextAlign, TextSize, FontStyle, TextRotation and Color before as you wish
yPos = xDefNum( yPos, 0 );
dx = xDefNum( dx, 1 );
if (dx <= 0) return;
yOff = xDefNum( yOff, -4 );
scale = xDefNum( scale, 1 );
digits = xDefNum( digits, 0 );
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, true );
aUnit = xDefStr( aUnit, '' );
if (this.AutoScalePix) yOff = this.ScalePix( yOff, this.ScalePixInt );
var ctr = this.CurrTrans;
var frame = this.GetFrame();
var oldAlign = this.TextVAlign;
var oldHPad = this.TextHPad;
var oldVPad = this.TextVPad;
this.SetTextVAlign( ((yOff < 0) ? 'top' : 'bottom') );
this.SetTextPadding( 0 );
if (frame.xmin > frame.xmax) { var tmp = frame.xmin; frame.xmin = frame.xmax; frame.xmax = tmp; }
var epsX = 1.0 / Math.abs(ctr.ScaleX);
// compute biggest textbox and increment dx so that boxes never collide
var box = this.GetTextSize( this.MakeLabel( frame.xmin, scale, digits, aUnit )+'m' );
var maxw = box.w;
box = this.GetTextSize( this.MakeLabel( frame.xmax, scale, digits, aUnit )+'m' );
if (box.w > maxw) maxw = box.w;
var ddx = (Math.floor(maxw/dx) + 1) * dx;
var oldObjTransEnable = this.ObjTrans.Enable( false );
var y = ctr.InvTransY( ctr.TransY( yPos ) - yOff );
if (frame.xmax >= 0) {
var x = this.StartXright( ((skipZero) ? ddx : 0), ddx, frame.xmin );
var xEnd = frame.xmax + epsX;
if (skipLimit) xEnd -= ddx;
while (x <= xEnd) {
this.Text( this.MakeLabel( x, scale, digits, aUnit ), x, y );
x += ddx;
}
}
if (frame.xmin <= 0) {
var x = this.StartXleft( -ddx, ddx, frame.xmax );
var xEnd = frame.xmin - epsX;
if (skipLimit) xEnd += ddx;
while (x >= xEnd) {
this.Text( this.MakeLabel( x, scale, digits, aUnit ), x, y );
x -= ddx;
}
}
this.SetTextVAlign( oldAlign );
this.SetTextPadding( oldHPad, oldVPad );
this.ObjTrans.Enable( oldObjTransEnable );
}
JsGraph.prototype.TicLabelsY = function( xPos, dy, xOff, scale, digits, skipZero, skipLimit, aUnit ) {
// xPos: real x coordinate of y axes
// dy: real distance between two tics on y axes
// xOff: real offset in pixels: positiv for label xOff right of xPos, negativ for left of xPos
// labels can be scaled by a factor of scale; use 1 if no scaling is required
// digits: int [0..20]. specifies number of digits after decimal point
// set TextVAlign, TextSize, FontStyle and Color before as you wish
xPos = xDefNum( xPos, 0 );
dy = xDefNum( dy, 1 );
if (dy <= 0) return;
xOff = xDefNum( xOff, -4 );
scale = xDefNum( scale, 1 );
digits = xDefNum( digits, 0 );
skipZero = xDefBool( skipZero, true );
skipLimit = xDefBool( skipLimit, true );
aUnit = xDefStr( aUnit, '' );
if (this.AutoScalePix) xOff = this.ScalePix( xOff, this.ScalePixInt );
var ctr = this.CurrTrans;
var frame = this.GetFrame();
var oldAlign = this.TextHAlign;
var oldHPad = this.TextHPad;
var oldVPad = this.TextVPad;
this.SetTextHAlign( ((xOff < 0) ? 'right' : 'left') );
this.SetTextPadding( 0 );
if (frame.ymin > frame.ymax) { var tmp = frame.ymin; frame.ymin = frame.ymax; frame.ymax = tmp; }
var epsY = 1.0 / Math.abs(ctr.ScaleY);
// compute biggest textbox and increment dy so that boxes never collide
var box = this.GetTextSize( this.MakeLabel( frame.ymax, scale, digits, aUnit ) );
var maxh = box.h;
var ddy = (Math.floor(maxh/dy) + 1) * dy;
var oldObjTransEnable = this.ObjTrans.Enable( false );
var x = ctr.InvTransX( ctr.TransX( xPos ) + xOff );
if (frame.ymax >= 0) {
var y = this.StartYup( ((skipZero) ? ddy : 0), ddy, frame.ymin );
var yEnd = frame.ymax + epsY;
if (skipLimit) yEnd -= ddy;
while (y <= yEnd) {
this.Text( this.MakeLabel( y, scale, digits, aUnit ), x, y );
y += ddy;
}
}
if (frame.ymin <= 0) {
var y = this.StartYdown( -ddy, ddy, frame.ymax );
var yEnd = frame.ymin - epsY;
if (skipLimit) yEnd += ddy;
while (y >= yEnd) {
this.Text( this.MakeLabel( y, scale, digits, aUnit ), x, y );
y -= ddy;
}
}
this.SetTextHAlign( oldAlign );
this.SetTextPadding( oldHPad, oldVPad );
this.ObjTrans.Enable( oldObjTransEnable );
}
JsGraph.prototype.MakeMarkers = function() {
// coordinates in viewport
this.MarkerName = [ 'ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp', 'Circle', 'Square', 'Diamond', 'Triangle', 'Triangle2', 'Star4', 'Star5', 'Star6', 'Plus', 'Cross', 'Star', 'Arrow1', 'Arrow2' ];
this.Markers = {
ArrowLeft: [ { type: 'Polygon', x: [ 0, 1, 1 ], y: [ 0, 0.5, -0.5 ] } ],
ArrowRight: [ { type: 'Polygon', x: [ 0, -1, -1 ], y: [ 0, -0.5, 0.5 ] } ],
ArrowDown: [ { type: 'Polygon', x: [ 0, 0.5, -0.5 ], y: [ 0, -1, -1 ] } ],
ArrowUp: [ { type: 'Polygon', x: [ 0, -0.5, 0.5 ], y: [ 0, 1, 1 ] } ],
Circle: [ { type: 'Circle', x: 0, y: 0, r: -0.5 } ],
Square: [ { type: 'Polygon', x: [ -0.5, 0.5, 0.5, -0.5 ], y: [ 0.5, 0.5, -0.5, -0.5 ] } ],
Diamond: [ { type: 'Polygon', x: [ 0, 0.5, 0, -0.5 ], y: [ 0.5, 0, -0.5, 0 ] } ],
Triangle: [ { type: 'Polygon', x: [ -0.5, 0.5, 0 ], y: [ 0.289, 0.289, -0.577 ] } ],
Triangle2: [ { type: 'Polygon', x: [ 0, 0.5, -0.5 ], y: [ 0.577, -0.289, -0.289 ] } ],
Star4: [ { type: 'Polygon', x: [ 0.5, 0.125, 0, -0.125, -0.5, -0.125, 0, 0.125 ], y: [ 0, -0.125, -0.5, -0.125, 0, 0.125, 0.5, 0.125 ] } ],
Star5: [ { type: 'Polygon', x: [ 0, -0.112, -0.433, -0.182, -0.294, 0, 0.294, 0.182, 0.475, 0.112 ], y: [ -0.5, -0.155, -0.155, 0.059, 0.405, 0.155, 0.405, 0.059, -0.155, -0.155 ] } ],
Star6: [ { type: 'Polygon', x: [ 0, -0.145, -0.433, -0.25, -0.433, -0.145, 0, 0.145, 0.433, 0.25, 0.433, 0.145 ], y: [ -0.5, -0.25, -0.25, 0, 0.25, 0.25, 0.5, 0.25, 0.25, 0, -0.25, -0.25 ] } ],
Plus: [ { type: 'Line', x1: -0.5, y1: 0, x2: 0.5, y2: 0 }, { type: 'Line', x1: 0, y1: -0.5, x2: 0, y2: 0.5 } ],
Cross: [ { type: 'Line', x1: -0.5, y1: 0.5, x2: 0.5, y2: -0.5 }, { type: 'Line', x1: -0.5, y1: -0.5, x2: 0.5, y2: 0.5 } ],
Star: [ { type: 'Line', x1: -0.5, y1: 0, x2: 0.5, y2: 0 },
{ type: 'Line', x1: -0.25, y1: -0.433, x2: 0.25, y2: 0.433 },
{ type: 'Line', x1: -0.25, y1: 0.433, x2: 0.25, y2: -0.433 } ],
Arrow1: [ { type: 'Polygon', x: [ 0, -1.5, -1.5 ], y: [ 0, -0.375, 0.375 ] } ],
Arrow2: [ { type: 'Polygon', x: [ 0, -1.5, -1.25, -1.5 ], y: [ 0, -0.375, 0, 0.375 ] } ]
};
}
JsGraph.prototype.ScaleAndMovePoly = function( poly, scale, moveX, moveY ) {
var len = poly.Size;
for (var i = 0; i < len; i++) {
poly.X[i] = poly.X[i] * scale + moveX;
poly.Y[i] = poly.Y[i] * scale + moveY;
}
}
JsGraph.prototype.ScaleAndMoveCoord = function( coord, scale, move ) {
return coord * scale + move;
}
JsGraph.prototype.Marker = function( x, y, mode, mat, size ) {
// Marker( x:Number, y:Number, mode, mat )
// Marker( pt:JsgVect2, mode, mat )
// Marker( xArr:Array, yArr:Array, mode, mat, size )
// Marker( poly:JsgPolygon, mode, mat )
//
// mode: int: 1 -> border, 2 -> fill, 3 -> fill and border, 4 -> inverse
// mat: JsgMat2 (optional) -> additional transformation matrix (e.g. rotation)
// Use RotationMatrixToVect( x, y ) to create mat
if (JsgPolygon.Ok(x)) return this.Marker( x.X, x.Y, y, mode, x.Size );
if (xArray(x) && xArray(y)) {
size = xDefNum( size, x.length );
for (var i = 0; i < size; i++) {
this.Marker( x[i], y[i], mode, mat );
}
return this;
}
if (JsgVect2.Ok(x)) return this.Marker( x[0], x[1], y, mode );
mode = xDefNum( mode, 3 );
var ctr = this.CurrTrans;
var otr = this.ObjTrans;
ctr.ObjTransXY( this.GetObjTrans(), x, y );
var oldTrans = this.SelectTrans('canvas');
var oldObjTransEnable = otr.Enable( false );
var symbol = this.Markers[this.MarkerSymbol]; // as array of elements
// invert marker sequence and contours if bit 3 is set in mode
var ix = 0;
var deltaIx = 1;
var inverse = false;
if (mode & 4) {
ix = symbol.length - 1;
deltaIx = -1;
inverse = true;
}
var drawMode = mode & 3;
for ( var i = 0; i < symbol.length; i++ ) {
var element = symbol[ix];
if (element.type == 'Polygon') {
var poly = this.WorkPoly.Reset();
var len = element.x.length;
for (var j = 0; j < len; j++) poly.AddPoint( element.x[j], element.y[j] );
if (JsgMat2.Ok(mat)) {
JsgMat2.TransPolyXY( mat, poly.X, poly.Y, poly.Size );
}
this.ScaleAndMovePoly( poly, this.DriverMarkerSize, ctr.x, ctr.y );
if (inverse) poly.Invert();
// drawMode + 4 = close poly
this.Polygon( poly, drawMode+4 );
}
else if (element.type == 'Line') {
var poly = this.WorkPoly.Reset();
poly.AddPoint( element.x1, element.y1 );
poly.AddPoint( element.x2, element.y2 );
if (JsgMat2.Ok(mat)) {
JsgMat2.TransPolyXY( mat, poly.X, poly.Y, poly.Size );
}
this.ScaleAndMovePoly( poly, this.DriverMarkerSize, ctr.x, ctr.y );
this.Line( poly.X[0], poly.Y[0], poly.X[1], poly.Y[1] );
}
else if (element.type == 'Circle') {
var cx = this.ScaleAndMoveCoord( element.x, this.DriverMarkerSize, ctr.x );
var cy = this.ScaleAndMoveCoord( element.y, this.DriverMarkerSize, ctr.y );
var cr = element.r * this.DriverMarkerSize;
if (inverse) cr *= -1;
this.Circle( cx, cy, cr, drawMode );
}
ix += deltaIx;
}
otr.Enable( oldObjTransEnable );
this.SelectTrans( oldTrans );
return this;
}
//------------------------------------------------
// JsgHtmlTextHandler implements the Html Text functions
// It uses the ClippingDiv object to strore text in div elements.
function JsgHtmlTextHandler( clippingDiv, canvas, context2d ) {
this.ClippingDiv = clippingDiv;
this.Canvas = canvas;
this.Context2D = context2d;
this.TextHAlign = 'left';
this.TextVAlign = 'top';
this.TextHPad = 0;
this.TextVPad = 0;
this.WorkRect = new JsgRect( 0, 0, 0, 0 );
this.Text = [];
this.Cache = [];
this.CachePtr = 0;
// create TextStyle and TextClass property and copy styles from clippingDiv
// If TextClass is defined, TextClass is assigned to Text div's and TextStyles are ignored
this.TextClass = '';
this.TextStyles = this.NewTextStyles();
}
// These properties are applied to text div's that are rendered with DrawText.
JsgHtmlTextHandler.AppliedTextStyles = 'color fontFamily fontSize fontStyle fontWeight lineHeight textAlign'.split(' ');
JsgHtmlTextHandler.prototype.NewTextStyles = function( from ) {
// from: { styles }
var styles = {};
var styleNames = JsgHtmlTextHandler.AppliedTextStyles;
for (var i = 0; i < styleNames.length; i++) {
styles[styleNames[i]] = '';
}
if (xObj(from)) this.CopyTextStyles( from, styles );
return styles;
}
JsgHtmlTextHandler.prototype.CopyTextStyles = function( src, dest ) {
// src, dest: { styles }
var styleNames = JsgHtmlTextHandler.AppliedTextStyles;
for (var i = 0; i < styleNames.length; i++) {
var name = styleNames[i];
if (src[name] != '') dest[name] = src[name];
}
}
JsgHtmlTextHandler.prototype.SameTextStyles = function( styles1, styles2 ) {
// styles1, styles2: { styles }
var styleNames = JsgHtmlTextHandler.AppliedTextStyles;
for (var i = 0; i < styleNames.length; i++) {
var name = styleNames[i];
if (styles1[name] != styles2[name]) return false;
}
return true;
}
JsgHtmlTextHandler.prototype.Clear = function() {
for (var i = 0; i < this.Text.length; i++) {
this.ClippingDiv.removeChild(this.Text[i]);
}
this.Text = [];
this.ResetCache();
}
JsgHtmlTextHandler.prototype.ClearCache = function() {
this.Cache = [];
this.CachePtr = 0;
}
JsgHtmlTextHandler.prototype.ResetCache = function() {
this.CachePtr = 0;
}
JsgHtmlTextHandler.prototype.FindTextSizeInCache = function( s, textClass, styles, aw, box ) {
// returns true, if s with textClass, styles and aw is found in cache
// box: JsgRect; modifies w and h with found cached size
if (this.CachePtr >= this.Cache.length) return false;
aw = xDefNum( aw, -1 );
var c = this.Cache[this.CachePtr];
if (c.Text == s && c.TextClass == textClass && this.SameTextStyles(c.Styles,styles) && c.ArgWidth == aw) {
this.CachePtr++;
box.SetSize( c.Width, c.Height );
return true;
}
this.ClearCache();
return false;
}
JsgHtmlTextHandler.prototype.AddToCache = function( s, textClass, styles, aw, width, height ) {
var stylesCopy = this.NewTextStyles( styles );
aw = xDefNum( aw, -1 );
this.Cache.push( { Text: s, TextClass: textClass, Styles: stylesCopy, ArgWidth: aw, Width: width, Height: height } );
this.CachePtr++;
}
JsgHtmlTextHandler.prototype.CreateTextNode = function( s, w ) {
var txt = document.createElement('div');
// apply classes and styles
this.CopyTextStyles( this.TextStyles, txt.style )
if (this.TextClass == '') {
txt.style.margin = '0';
txt.style.padding = '0';
} else {
txt.className = this.TextClass;
}
txt.style.position = 'absolute';
txt.style.boxSizing = 'border-box';
if (w > 0) txt.style.width = w+'px';
txt.innerHTML = s;
return txt;
}
JsgHtmlTextHandler.prototype.GetTextSize = function ( s, w, box ) {
// Computes size of Textbox without reformating. Compare with GetTextBox.
// box: JsgRect; modifies w and h but not x and y
if (this.FindTextSizeInCache( s, this.TextClass, this.TextStyles, w, box )) return;
var txtNode = this.CreateTextNode( s, w );
txtNode.style.visibility = 'hidden';
this.ClippingDiv.appendChild( txtNode );
box.SetSize( txtNode.offsetWidth, txtNode.offsetHeight );
this.ClippingDiv.removeChild( txtNode );
this.AddToCache( s, this.TextClass, this.TextStyles, w, box.w, box.h );
}
JsgHtmlTextHandler.prototype.GetTextBox = function( s, x, y, w, box ) {
// box: JsgRect; modifies x, y, w and h
return this.HandleText( 0, s, x, y, w, box );
}
JsgHtmlTextHandler.prototype.DrawText = function( s, x, y, w ) {
this.HandleText( 1, s, x, y, w, this.WorkRect );
}
JsgHtmlTextHandler.prototype.HandleText = function( mode, s, x, y, w, box ) {
// mode = 0 -> compute size of textbox without drawing the text
// mode = 1 -> compute size of textbox and draw text
// box: JsgRect; modifies x, y, w and h
// Note: Text may be reformated if overlaping with canvas. Compare with GetTextSize.
// if w > 0 it specifies the wished text width without padding
this.GetTextSize( s, w, box );
box.w += 2 * this.TextHPad;
box.h += 2 * this.TextVPad;
var top = y;
var left = x;
var padleft = this.TextHPad;
var padright = this.TextHPad;
if (this.TextHAlign == 'center') left -= box.w / 2;
if (this.TextHAlign == 'right') left -= box.w;
if (this.TextVAlign == 'middle') top -= box.h / 2;
if (this.TextVAlign == 'bottom') top -= box.h;
// recompute box if w = 0 and text box overlaps ClippingDiv border left or right
if (w == 0) {
var cw = this.ClippingDiv.offsetWidth;
var right = left + box.w;
var newleft = left;
var newright = right;
var borderCrossed = false;
if (left < 0 && right > 0) {
// crossing left clipping border; reduce width from left
padleft = this.TextHPad + left;
if (padleft < 0) padleft = 0;
newleft = 0;
borderCrossed = true;
}
if (left < cw && right > cw) {
// crossing right clipping border; reduce with from right (additionally)
padright -= right - cw;
if (padright < 0) padright = 0;
newright = cw;
borderCrossed = true;
}
// of box not outside clipping div then compute new width w
if (borderCrossed && newright > 0 && newleft < cw) {
w = newright - newleft - padleft - padright;
if (w < 0) w = 0;
}
if (w > 0) {
// if w is adjusted, recompute textbox geom and position this time with new width
var top = y;
var left = newleft;
this.GetTextSize( s, w, box );
box.w += padleft + padright;
box.h += 2 * this.TextVPad;
if (this.TextVAlign == 'middle') top -= box.h / 2;
if (this.TextVAlign == 'bottom') top -= box.h;
}
}
box.SetPos( left, top );
if (mode == 1) {
var txtNode = this.CreateTextNode( s, w );
txtNode.style.left = left + padleft + 'px';
txtNode.style.top = top + this.TextVPad + 'px';
this.ClippingDiv.appendChild( txtNode );
this.Text.push( txtNode );
}
}