// 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 ); } }