You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3631 lines
119 KiB
Plaintext
3631 lines
119 KiB
Plaintext
/**
|
|
* Creates an instance of Point
|
|
*
|
|
*
|
|
* @constructor
|
|
* @this {Point}
|
|
* @param {Number} x The x coordinate of point.
|
|
* @param {Number} y The y coordinate of point.
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
* Note: Even if it is named Point this class should be named Dot as Dot is closer
|
|
* then Point from math perspective.
|
|
**/
|
|
function Point(x, y){
|
|
/**The x coordinate of point*/
|
|
this.x = x;
|
|
|
|
/**The y coordinate of point*/
|
|
this.y = y;
|
|
|
|
/**The {@link Style} of the Point*/
|
|
this.style = new Style();
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'Point'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {Point} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Point} a newly constructed Point
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Point.load = function(o){
|
|
var newPoint = new Point(Number(o.x), Number(o.y));
|
|
newPoint.style = Style.load(o.style);
|
|
return newPoint;
|
|
}
|
|
|
|
|
|
/**Creates an array of points from an array of {JSONObject}s
|
|
*@param {Array} v - the array of JSONObjects
|
|
*@return an {Array} of {Point}s
|
|
**/
|
|
Point.loadArray = function(v){
|
|
var newPoints = [];
|
|
for(var i=0; i< v.length; i++){
|
|
newPoints.push(Point.load(v[i]));
|
|
}
|
|
return newPoints;
|
|
}
|
|
|
|
|
|
/**Clones an array of points
|
|
*@param {Array} v - the array of {Point}s
|
|
*@return an {Array} of {Point}s
|
|
**/
|
|
Point.cloneArray = function(v){
|
|
var newPoints = [];
|
|
for(var i=0; i< v.length; i++){
|
|
newPoints.push(v[i].clone());
|
|
}
|
|
return newPoints;
|
|
}
|
|
|
|
Point.prototype = {
|
|
constructor : Point,
|
|
|
|
/*
|
|
*Transform a point by a tranformation matrix.
|
|
*It is done by multiplication
|
|
*Pay attention on the order of multiplication: The tranformation {Matrix} is
|
|
*multiplied with the {Point} matrix.
|
|
* P' = M x P
|
|
*@param matrix is a 3x3 matrix
|
|
*@see <a href="http://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations">http://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations</a>
|
|
**/
|
|
transform:function(matrix){
|
|
if(this.style!=null){
|
|
this.style.transform(matrix);
|
|
}
|
|
var oldX = this.x;
|
|
var oldY = this.y;
|
|
this.x = matrix[0][0] * oldX + matrix[0][1] * oldY + matrix[0][2];
|
|
this.y = matrix[1][0] * oldX + matrix[1][1] * oldY + matrix[1][2];
|
|
},
|
|
|
|
/**Paint current {Point} withing a context
|
|
*If you want to use a different style then the default one change the style
|
|
**/
|
|
paint:function(context){
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
if(this.style.strokeStyle != ""){
|
|
context.fillStyle = this.style.strokeStyle;
|
|
context.beginPath();
|
|
var width = 1;
|
|
if(this.style.lineWidth != null){
|
|
width = parseInt(this.style.lineWidth);
|
|
}
|
|
context.arc(this.x, this.y, width, 0,Math.PI/180*360,false);
|
|
context.fill();
|
|
}
|
|
},
|
|
|
|
|
|
/**Tests if this point is similar to other point
|
|
*@param {Point} anotherPoint - the other point
|
|
**/
|
|
equals:function(anotherPoint){
|
|
if(! (anotherPoint instanceof Point) ){
|
|
return false;
|
|
}
|
|
return (this.x == anotherPoint.x)
|
|
&& (this.y == anotherPoint.y)
|
|
&& this.style.equals(anotherPoint.style);
|
|
},
|
|
|
|
/**Clone current Point
|
|
**/
|
|
clone: function(){
|
|
var newPoint = new Point(this.x, this.y);
|
|
newPoint.style = this.style.clone();
|
|
return newPoint;
|
|
},
|
|
|
|
/**Tests to see if a point (x, y) is within a range of current Point
|
|
*@param {Numeric} x - the x coordinate of tested point
|
|
*@param {Numeric} y - the x coordinate of tested point
|
|
*@param {Numeric} radius - the radius of the vicinity
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
near:function(x, y, radius){
|
|
var distance = Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2));
|
|
|
|
return (distance <= radius);
|
|
},
|
|
|
|
contains: function(x,y){
|
|
return this.x == x && this.y == y;
|
|
},
|
|
|
|
toString:function(){
|
|
return 'point(' + this.x + ',' + this.y + ')';
|
|
},
|
|
|
|
getPoints:function(){
|
|
return [this];
|
|
},
|
|
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
/**
|
|
*We will draw a point a circle. The "visual" color and thicknes of the point will
|
|
*be created by the SVG's element style
|
|
*
|
|
*@see <a href="http://tutorials.jenkov.com/svg/circle-element.html">http://tutorials.jenkov.com/svg/circle-element.html</a>
|
|
*
|
|
*Example:
|
|
*<circle cx="40" cy="40" r="1" style="stroke:#006600; fill:#00cc00"/>
|
|
**/
|
|
toSVG: function(){
|
|
var r = '';
|
|
|
|
r += "\n" + repeat("\t", INDENTATION) + '<circle cx="' + this.x + '" cy="' + this.y + '" r="' + 1 + '"' ;
|
|
r += this.style.toSVG();
|
|
r += '/>';
|
|
|
|
return r;
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Creates an instance of a Line. A Line is actually a segment and not a pure
|
|
* geometrical Line
|
|
*
|
|
* @constructor
|
|
* @this {Line}
|
|
* @param {Point} startPoint - starting point of the line
|
|
* @param {Point} endPoint - the ending point of the line
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
function Line(startPoint, endPoint){
|
|
/**Starting {@link Point} of the line*/
|
|
this.startPoint = startPoint;
|
|
|
|
/**Ending {@link Point} of the line*/
|
|
this.endPoint = endPoint;
|
|
|
|
/**The {@link Style} of the line*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'Line'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {Line} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Line} a newly constructed Line
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Line.load = function(o){
|
|
var newLine = new Line(
|
|
Point.load(o.startPoint),
|
|
Point.load(o.endPoint)
|
|
);
|
|
|
|
newLine.style = Style.load(o.style);
|
|
return newLine;
|
|
}
|
|
|
|
Line.prototype = {
|
|
contructor: Line,
|
|
|
|
transform:function(matrix){
|
|
this.startPoint.transform(matrix);
|
|
this.endPoint.transform(matrix);
|
|
if(this.style!=null){
|
|
this.style.transform(matrix);
|
|
}
|
|
|
|
},
|
|
|
|
paint:function(context){
|
|
context.beginPath();
|
|
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
context.moveTo(this.startPoint.x, this.startPoint.y);
|
|
if(this.style.dashLength==0){
|
|
context.lineTo(this.endPoint.x, this.endPoint.y);
|
|
context.closePath(); // added for line's correct Chrome's displaying
|
|
}
|
|
else{
|
|
|
|
//get the length of the line
|
|
var lineLength=Math.sqrt(Math.pow(this.startPoint.x-this.endPoint.x,2)+Math.pow(this.startPoint.y-this.endPoint.y,2));
|
|
|
|
//get the angle
|
|
var angle = Util.getAngle(this.startPoint,this.endPoint);
|
|
|
|
//draw a dotted line
|
|
var move=false;
|
|
for(var i=0; i<lineLength; i+=(this.style.dashLength)){
|
|
var p = this.startPoint.clone();
|
|
|
|
//translate to origin of start
|
|
p.transform(Matrix.translationMatrix(-this.startPoint.x,-this.startPoint.y))
|
|
|
|
//move it north by incremental dashlengths
|
|
p.transform(Matrix.translationMatrix(0, -i));
|
|
|
|
//rotate to correct location
|
|
p.transform(Matrix.rotationMatrix(angle));
|
|
|
|
//translate back
|
|
p.transform(Matrix.translationMatrix(this.startPoint.x,this.startPoint.y))
|
|
|
|
if (move==false){
|
|
context.lineTo(p.x, p.y);
|
|
move=true;
|
|
}
|
|
else{
|
|
context.moveTo(p.x, p.y);
|
|
move=false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(this.style.strokeStyle != null && this.style.strokeStyle != ""){
|
|
context.stroke();
|
|
}
|
|
},
|
|
|
|
clone:function(){
|
|
var ret = new Line(this.startPoint.clone(), this.endPoint.clone());
|
|
ret.style = this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
equals:function(anotherLine){
|
|
if(!anotherLine instanceof Line){
|
|
return false;
|
|
}
|
|
return this.startPoint.equals(anotherLine.startPoint)
|
|
&& this.endPoint.equals(anotherLine.endPoint)
|
|
&& this.style.equals(anotherLine.style);
|
|
},
|
|
|
|
/** Tests to see if a point belongs to this line (not as infinite line but more like a segment)
|
|
* Algorithm: Compute line's equation and see if (x, y) verifies it.
|
|
* @param {Number} x - the X coordinates
|
|
* @param {Number} y - the Y coordinates
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
contains: function(x, y){
|
|
// if the point is inside rectangle bounds of the segment
|
|
if (Math.min(this.startPoint.x, this.endPoint.x) <= x
|
|
&& x <= Math.max(this.startPoint.x, this.endPoint.x)
|
|
&& Math.min(this.startPoint.y, this.endPoint.y) <= y
|
|
&& y <= Math.max(this.startPoint.y, this.endPoint.y)) {
|
|
|
|
// check for vertical line
|
|
if (this.startPoint.x == this.endPoint.x) {
|
|
return x == this.startPoint.x;
|
|
} else { // usual (not vertical) line can be represented as y = a * x + b
|
|
var a = (this.endPoint.y - this.startPoint.y) / (this.endPoint.x - this.startPoint.x);
|
|
var b = this.startPoint.y - a * this.startPoint.x;
|
|
return y == a * x + b;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/*
|
|
*See if we are near a {Line} by a certain radius (also includes the extremities into computation)
|
|
*@param {Number} x - the x coordinates
|
|
*@param {Number} y - the y coordinates
|
|
*@param {Number} radius - the radius to search for
|
|
*@see http://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
|
|
*@see "Mathematics for Computer Graphics, 2nd Ed., by John Vice, page 227"
|
|
*@author Zack Newsham <zack_newsham@yahoo.co.uk>
|
|
*@author Arty
|
|
*@author Alex
|
|
**/
|
|
near:function(x,y,radius){
|
|
|
|
if(this.endPoint.x === this.startPoint.x){ //Vertical line, so the vicinity area is a rectangle
|
|
return ( (this.startPoint.y-radius<=y && this.endPoint.y+radius>=y)
|
|
|| (this.endPoint.y-radius<=y && this.startPoint.y+radius>=y))
|
|
&& x > this.startPoint.x - radius && x < this.startPoint.x + radius ;
|
|
}
|
|
|
|
if(this.startPoint.y === this.endPoint.y){ //Horizontal line, so the vicinity area is a rectangle
|
|
return ( (this.startPoint.x - radius<=x && this.endPoint.x+radius>=x)
|
|
|| (this.endPoint.x-radius<=x && this.startPoint.x+radius>=x))
|
|
&& y>this.startPoint.y-radius && y<this.startPoint.y+radius ;
|
|
}
|
|
|
|
|
|
var startX = Math.min(this.endPoint.x,this.startPoint.x);
|
|
var startY = Math.min(this.endPoint.y,this.startPoint.y);
|
|
var endX = Math.max(this.endPoint.x,this.startPoint.x);
|
|
var endY = Math.max(this.endPoint.y,this.startPoint.y);
|
|
|
|
/*We will compute the distance from point to the line
|
|
* by using the algorithm from
|
|
* http://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
|
|
* */
|
|
|
|
//First we need to find a,b,c of the line equation ax + by + c = 0
|
|
var a = this.endPoint.y - this.startPoint.y;
|
|
var b = this.startPoint.x - this.endPoint.x;
|
|
var c = -(this.startPoint.x * this.endPoint.y - this.endPoint.x * this.startPoint.y);
|
|
|
|
//Secondly we get the distance "Mathematics for Computer Graphics, 2nd Ed., by John Vice, page 227"
|
|
var d = Math.abs( (a*x + b*y + c) / Math.sqrt(Math.pow(a,2) + Math.pow(b,2)) );
|
|
|
|
//Thirdly we get coordinates of closest line's point to target point
|
|
//http://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Cartesian_coordinates
|
|
var closestX = (b * (b*x - a*y) - a*c) / ( Math.pow(a,2) + Math.pow(b,2) );
|
|
var closestY = (a * (-b*x + a*y) - b*c) / ( Math.pow(a,2) + Math.pow(b,2) );
|
|
|
|
var r = ( d <= radius && endX>=closestX && closestX>=startX && endY>=closestY && closestY>=startY ) //the projection of the point falls INSIDE of the segment
|
|
|| this.startPoint.near(x,y,radius) || this.endPoint.near(x,y,radius); //the projection of the point falls OUTSIDE of the segment
|
|
|
|
return r;
|
|
|
|
},
|
|
|
|
/**we need to create a new array each time, or we will affect the actual shape*/
|
|
getPoints:function(){
|
|
var points = [];
|
|
points.push(this.startPoint);
|
|
points.push(this.endPoint);
|
|
return points;
|
|
},
|
|
|
|
/**Return the {Point} corresponding the t certain t value
|
|
* @param {Number} t the value of parameter t, where t in [0,1], t is like a percent*/
|
|
getPoint: function(t){
|
|
var Xp = t * (this.endPoint.x - this.startPoint.x) + this.startPoint.x;
|
|
var Yp = t * (this.endPoint.y - this.startPoint.y) + this.startPoint.y;
|
|
|
|
return new Point(Xp, Yp);
|
|
},
|
|
|
|
/**
|
|
* Returns the middle of the line
|
|
* @return {Point} the middle point
|
|
* */
|
|
getMiddle : function(){
|
|
return Util.getMiddle(this.startPoint, this.endPoint);
|
|
},
|
|
|
|
|
|
getLength : function(){
|
|
return Util.getLength(this.startPoint, this.endPoint);
|
|
},
|
|
|
|
/**
|
|
*Get bounds for this line
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
/**String representation*/
|
|
toString:function(){
|
|
return 'line(' + this.startPoint + ',' + this.endPoint + ')';
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive*/
|
|
toSVG:function(){
|
|
//<line x1="0" y1="0" x2="300" y2="300" style="stroke:rgb(99,99,99);stroke-width:2"/>
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<line x1="' + this.startPoint.x + '" y1="' + this.startPoint.y + '" x2="' + this.endPoint.x + '" y2="' + this.endPoint.y + '"';
|
|
result += this.style.toSVG();
|
|
result += " />"
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Creates an instance of a Polyline
|
|
*
|
|
* @constructor
|
|
* @this {Polyline}
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
function Polyline(){
|
|
/**An {Array} of {@link Point}s*/
|
|
this.points = [];
|
|
|
|
/**The {@link Style} of the polyline*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**The starting {@link Point}.
|
|
* Required for path, we could use getPoints(), but this existed first.
|
|
* Also its a lot simpler. Each other element used in path already has a startPoint
|
|
* //TODO: (added by alex) This is bullshit....we need to remove this kind of junky code
|
|
**/
|
|
this.startPoint = null;
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'Polyline'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {Polyline} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Polyline} a newly constructed Polyline
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Polyline.load = function(o){
|
|
var newPolyline = new Polyline();
|
|
newPolyline.points = Point.loadArray(o.points);
|
|
newPolyline.style = Style.load(o.style);
|
|
newPolyline.startPoint = Point.load(o.startPoint);
|
|
return newPolyline;
|
|
};
|
|
|
|
Polyline.prototype = {
|
|
constructor : Polyline,
|
|
|
|
addPoint:function(point){
|
|
if(this.points.length==0){
|
|
this.startPoint=point;
|
|
}
|
|
this.points.push(point);
|
|
|
|
// update bound coordinates for gradient
|
|
this.style.gradientBounds = this.getBounds();
|
|
},
|
|
|
|
transform:function(matrix){
|
|
if(this.style!=null){
|
|
this.style.transform(matrix);
|
|
}
|
|
for(var i=0; i<this.points.length; i++){
|
|
this.points[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
getPoints:function(){
|
|
return Point.cloneArray(this.points);
|
|
},
|
|
|
|
/**Return the {Point} corresponding the t certain t value.
|
|
* NOTE: t is the visual percentage of a point from the start of the
|
|
* primitive and the result is
|
|
* different than the t from getPoint(...) method
|
|
* @param {Number} t the value of parameter t, where t in [0,1]*/
|
|
getVisualPoint:function (t){
|
|
var l = this.getLength();
|
|
|
|
|
|
var walked = 0;
|
|
var i;
|
|
for(i=0; i< this.points.length-1; i++){
|
|
if( walked + Util.distance(this.points[i], this.points[i+1]) > l * t ){
|
|
break;
|
|
}
|
|
|
|
walked += Util.distance(this.points[i], this.points[i+1]);
|
|
}
|
|
|
|
var rest = l * t - walked;
|
|
var currentSegmentLength = Util.distance(this.points[i], this.points[i+1]);
|
|
|
|
//find the position/ration of the middle of Polyline on current segment
|
|
var segmentPercent = rest / currentSegmentLength;
|
|
var THEpoint = new Line(this.points[i], this.points[i+1]).getPoint(segmentPercent);
|
|
|
|
return THEpoint;
|
|
},
|
|
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
clone:function(){
|
|
var ret=new Polyline();
|
|
for(var i=0; i<this.points.length; i++){
|
|
ret.addPoint(this.points[i].clone());
|
|
}
|
|
ret.style=this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
/**Return the length of the polyline
|
|
* by summing up all the length of all
|
|
* segments*/
|
|
getLength : function(){
|
|
var l = 0;
|
|
for(var i=0; i< this.points.length-1; i++){
|
|
l += Util.distance(this.points[i], this.points[i+1]);
|
|
}
|
|
|
|
return l;
|
|
},
|
|
|
|
|
|
equals:function(anotherPolyline){
|
|
if(!anotherPolyline instanceof Polyline){
|
|
return false;
|
|
}
|
|
if(anotherPolyline.points.length == this.points.length){
|
|
for(var i=0; i<this.points.length; i++){
|
|
if(!this.points[i].equals(anotherPolyline.points[i])){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
return false;
|
|
}
|
|
|
|
if(!this.style.equals(anotherPolyline.style)){
|
|
return false;
|
|
}
|
|
|
|
if(!this.startPoint.equals(anotherPolyline.startPoint)){
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
|
|
Log.info("Polyline:paint() start");
|
|
context.beginPath();
|
|
context.moveTo(this.points[0].x, this.points[0].y);
|
|
for(var i=1; i<this.points.length; i++){
|
|
context.lineTo(this.points[i].x, this.points[i].y);
|
|
//Log.info("Polyline:paint()" + " Paint a line to [" + this.points[i].x + ',' + this.points[i].y + ']');
|
|
}
|
|
|
|
|
|
if(this.style.fillStyle!=null && this.style.fillStyle!=""){
|
|
context.fill();
|
|
//Log.info("Polyline:paint() We have fill: " + this.style.fillStyle)
|
|
}
|
|
|
|
if(this.style.strokeStyle !=null && this.style.strokeStyle != ""){
|
|
//Log.info("Polyline:paint() We have stroke: " + this.style.strokeStyle)
|
|
context.strokeStyle = this.style.strokeStyle;
|
|
context.stroke();
|
|
}
|
|
},
|
|
|
|
|
|
contains:function(x, y){
|
|
return Util.isPointInside(new Point(x, y), this.getPoints())
|
|
},
|
|
|
|
|
|
near:function(x, y, radius){
|
|
for(var i=0; i< this.points.length-1; i++){
|
|
var l = new Line(this.points[i], this.points[i+1]);
|
|
|
|
if(l.near(x,y,radius)){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
toString:function(){
|
|
var result = 'polyline(';
|
|
for(var i=0; i < this.points.length; i++){
|
|
result += this.points[i].toString() + ' ';
|
|
}
|
|
result += ')';
|
|
return result;
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive*/
|
|
toSVG:function(){
|
|
//<polyline points="0,0 0,20 20,20 20,40 40,40 40,60" style="fill:white;stroke:red;stroke-width:2"/>
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<polyline points="';
|
|
for(var i=0; i < this.points.length; i++){
|
|
result += this.points[i].x + ',' + this.points[i].y + ' ';
|
|
}
|
|
result += '"';
|
|
result += this.style.toSVG();
|
|
result += '/>';
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an instance of a Polygon
|
|
*
|
|
* @constructor
|
|
* @this {Polygon}
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
function Polygon(){
|
|
/**An {Array} of {@link Point}s*/
|
|
this.points = [];
|
|
|
|
/**The {@link Style} of the polygon*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'Polygon'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {Polygon} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Polygon} a newly constructed Polygon
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Polygon.load = function(o){
|
|
var newPolygon = new Polygon();
|
|
newPolygon.points = Point.loadArray(o.points);
|
|
newPolygon.style = Style.load(o.style);
|
|
return newPolygon;
|
|
}
|
|
|
|
|
|
Polygon.prototype = {
|
|
contructor : Polygon,
|
|
|
|
addPoint:function(point){
|
|
this.points.push(point);
|
|
|
|
// update bound coordinates for gradient
|
|
this.style.gradientBounds = this.getBounds();
|
|
},
|
|
|
|
|
|
getPosition:function(){
|
|
return [this.points[0].x,[this.points[0].y]];
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
context.beginPath();
|
|
if(this.style!=null){
|
|
this.style.setupContext(context);
|
|
}
|
|
if(this.points.length > 1){
|
|
context.moveTo(this.points[0].x, this.points[0].y);
|
|
for(var i=1; i<this.points.length; i++){
|
|
context.lineTo(this.points[i].x, this.points[i].y)
|
|
}
|
|
}
|
|
context.closePath();
|
|
|
|
//fill current path
|
|
if(this.style.fillStyle != null && this.style.fillStyle != ""){
|
|
context.fill();
|
|
}
|
|
|
|
//stroke current path
|
|
if(this.style.strokeStyle != null && this.style.strokeStyle != ""){
|
|
context.stroke();
|
|
}
|
|
},
|
|
|
|
|
|
getPoints:function(){
|
|
var p = [];
|
|
for (var i=0; i<this.points.length; i++){
|
|
p.push(this.points[i]);
|
|
}
|
|
return p;
|
|
},
|
|
|
|
|
|
/**
|
|
*@return {Array<Number>} - returns [minX, minY, maxX, maxY] - bounds, where
|
|
* all points are in the bounds.*/
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
fill:function(context,color){
|
|
context.fillStyle=color;
|
|
context.beginPath();
|
|
context.moveTo(this.points[0].x, this.points[0].y);
|
|
for(var i=1; i<this.points.length; i++){
|
|
context.lineTo(this.points[i].x, this.points[i].y);
|
|
}
|
|
context.lineTo(this.points[0].x, this.points[0].y);
|
|
context.closePath();
|
|
context.fill();
|
|
},
|
|
|
|
near:function(x,y,radius){
|
|
var i=0;
|
|
for(i=0; i< this.points.length-1; i++){
|
|
var l = new Line(this.points[i], this.points[i+1]);
|
|
if(l.near(x,y,radius)){
|
|
return true;
|
|
}
|
|
}
|
|
l=new Line(this.points[i], this.points[0]);
|
|
if(l.near(x,y,radius)){
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
|
|
equals:function(anotherPolygon){
|
|
if(!anotherPolygon instanceof Polygon){
|
|
return false;
|
|
}
|
|
if(anotherPolygon.points.length == this.points.length){
|
|
for(var i=0; i<this.points.length; i++){
|
|
if(!this.points[i].equals(anotherPolygon.points[i])){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
//TODO: test for all Polygon members
|
|
return true;
|
|
},
|
|
|
|
clone:function(){
|
|
var ret=new Polygon();
|
|
for(var i=0; i<this.points.length; i++){
|
|
ret.addPoint(this.points[i].clone());
|
|
}
|
|
ret.style = this.style.clone();
|
|
|
|
return ret;
|
|
},
|
|
|
|
contains:function(x, y, includeBorders){
|
|
var inPath = false;
|
|
var p = new Point(x,y);
|
|
if(!p){
|
|
alert('Polygon: P is null');
|
|
}
|
|
|
|
if (includeBorders) {
|
|
return Util.isPointInsideOrOnBorder(p, this.points);
|
|
} else {
|
|
return Util.isPointInside(p, this.points);
|
|
}
|
|
},
|
|
|
|
transform:function(matrix){
|
|
if(this.style!=null){
|
|
this.style.transform(matrix);
|
|
}
|
|
for(var i=0; i < this.points.length; i++){
|
|
this.points[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
toString:function(){
|
|
var result = 'polygon(';
|
|
for(var i=0; i < this.points.length; i++){
|
|
result += this.points[i].toString() + ' ';
|
|
}
|
|
result += ')';
|
|
return result;
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive*/
|
|
toSVG:function(){
|
|
//<polygon points="220,100 300,210 170,250" style="fill:#cccccc; stroke:#000000;stroke-width:1"/>
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<polygon points="';
|
|
for(var i=0; i < this.points.length; i++){
|
|
result += this.points[i].x + ',' + this.points[i].y + ' ';
|
|
}
|
|
result += '" '
|
|
//+ 'style="fill:#cccccc;stroke:#000000;stroke-width:1"'
|
|
+ this.style.toSVG()
|
|
+ ' />';
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an instance of a DottedPolygon.
|
|
* DottedPolygon is a poligon with all edges dotted or with a certain pattern.
|
|
* As for now (Summer 2012) context does not support lines with a pattern
|
|
* this had to be created
|
|
*
|
|
* @constructor
|
|
* @this {DottedPolygon}
|
|
* @param {Array} pattern - an array of dots and spaces Ex: [10,2,2 4,7] means 10 dotts, 2 spaces, 2 dots and 7 spaces.
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
function DottedPolygon(pattern){
|
|
/**An {Array} of {@link Point}s*/
|
|
this.points = [];
|
|
|
|
/**The {@link Style} of the polygon*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**An {Array} of {Integer}s*/
|
|
this.pattern = pattern;
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'DottedPolygon'; //object type used for JSON deserialization
|
|
}
|
|
|
|
|
|
|
|
|
|
/**Creates a {Polygon} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Polygon} a newly constructed Polygon
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
DottedPolygon.load = function(o){
|
|
var newPolygon = new DottedPolygon(o.pattern);
|
|
newPolygon.points = Point.loadArray(o.points);
|
|
newPolygon.style = Style.load(o.style);
|
|
return newPolygon;
|
|
}
|
|
|
|
|
|
DottedPolygon.prototype = {
|
|
contructor : DottedPolygon,
|
|
|
|
addPoint:function(point){
|
|
this.points.push(point);
|
|
},
|
|
|
|
|
|
getPosition:function(){
|
|
return [this.points[0].x,[this.points[0].y]];
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
|
|
//simply ignore anything that don't have at least 2 points
|
|
if(this.points.length < 2){
|
|
return;
|
|
}
|
|
|
|
var clonnedPoints = Point.cloneArray(this.points);
|
|
|
|
//first fill
|
|
if(this.style.fillStyle != null && this.style.fillStyle != ""){
|
|
context.beginPath();
|
|
context.moveTo(clonnedPoints[0].x, clonnedPoints[0].y);
|
|
for(i=1;i<clonnedPoints.length; i++){
|
|
context.lineTo(clonnedPoints[i].x, clonnedPoints[i].y);
|
|
}
|
|
context.fill();
|
|
}
|
|
|
|
//then stroke
|
|
if(this.style.strokeStyle != null && this.style.strokeStyle != ""){
|
|
context.beginPath(); //begin a new path
|
|
Util.decorate(context, clonnedPoints, this.pattern);
|
|
context.stroke();
|
|
}
|
|
|
|
//context.restore();
|
|
},
|
|
|
|
|
|
getPoints:function(){
|
|
var p = [];
|
|
for (var i=0; i<this.points.length; i++){
|
|
p.push(this.points[i]);
|
|
}
|
|
return p;
|
|
},
|
|
|
|
|
|
/**
|
|
*@return {Array<Number>} - returns [minX, minY, maxX, maxY] - bounds, where
|
|
* all points are in the bounds.*/
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
|
|
near:function(x,y,radius){
|
|
var i=0;
|
|
for(i=0; i< this.points.length-1; i++){
|
|
var l=new Line(this.points[i], this.points[i+1]);
|
|
if(l.near(x,y,radius)){
|
|
return true;
|
|
}
|
|
}
|
|
l = new Line(this.points[i], this.points[0]);
|
|
if(l.near(x,y,radius)){
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
|
|
equals:function(anotherPolygon){
|
|
if(!anotherPolygon instanceof DottedPolygon){
|
|
return false;
|
|
}
|
|
if(anotherPolygon.points.length == this.points.length){
|
|
for(var i=0; i<this.points.length; i++){
|
|
if(!this.points[i].equals(anotherPolygon.points[i])){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
//TODO: test for all DottedPolygon's pattern
|
|
return true;
|
|
},
|
|
|
|
|
|
clone:function(){
|
|
var ret = new DottedPolygon();
|
|
for(var i=0; i<this.points.length; i++){
|
|
ret.addPoint(this.points[i].clone());
|
|
}
|
|
ret.style=this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
|
|
contains:function(x, y){
|
|
var inPath = false;
|
|
var p = new Point(x,y);
|
|
if(!p){
|
|
alert('DottedPolygon: P is null');
|
|
}
|
|
|
|
return Util.isPointInside(p, this.points);
|
|
},
|
|
|
|
transform:function(matrix){
|
|
if(this.style != null){
|
|
this.style.transform(matrix);
|
|
}
|
|
for(var i=0; i < this.points.length; i++){
|
|
this.points[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
toString:function(){
|
|
var result = 'dottedpolygon(';
|
|
for(var i=0; i < this.points.length; i++){
|
|
result += this.points[i].toString() + ' ';
|
|
}
|
|
result += ')';
|
|
return result;
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive*/
|
|
toSVG:function(){
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<text x="20" y="40">DottedPolygon:toSVG() - no implemented</text>';
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an instance of a quad curve.
|
|
* A curved line determined by 2 normal points (startPoint and endPoint) and 1 control point (controlPoint)
|
|
*
|
|
* @constructor
|
|
* @this {QuadCurve}
|
|
* @param {Point} startPoint - starting point of the line
|
|
* @param {Point} controlPoint - the control point of the line
|
|
* @param {Point} endPoint - the ending point of the line
|
|
* @see <a href="http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves">http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves</a>
|
|
**/
|
|
function QuadCurve(startPoint, controlPoint, endPoint){
|
|
/**The start {@link Point}*/
|
|
this.startPoint = startPoint;
|
|
|
|
/**The controll {@link Point}*/
|
|
this.controlPoint = controlPoint;
|
|
|
|
/**The end {@link Point}*/
|
|
this.endPoint = endPoint;
|
|
|
|
/**The {@link Style} of the quad*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'QuadCurve'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {QuadCurve} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {QuadCurve} a newly constructed QuadCurve
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
QuadCurve.load = function(o){
|
|
var newQuad = new QuadCurve(
|
|
Point.load(o.startPoint),
|
|
Point.load(o.controlPoint),
|
|
Point.load(o.endPoint)
|
|
);
|
|
|
|
newQuad.style = Style.load(o.style);
|
|
return newQuad;
|
|
};
|
|
|
|
/**Creates an {Array} of {QuadCurve} out of JSON parsed object
|
|
*@param {JSONObject} v - the JSON parsed object (actually an {Array} of {JSONObject}s
|
|
*@return {Array} of {QuadCurve}s
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
QuadCurve.loadArray = function(v){
|
|
var quads = [];
|
|
|
|
for(var i=0; i<v.length; i++){
|
|
quads.push(QuadCurve.load(v[i]));
|
|
}
|
|
|
|
return quads;
|
|
};
|
|
|
|
QuadCurve.prototype = {
|
|
constructor : QuadCurve,
|
|
|
|
transform:function(matrix){
|
|
if(this.style!=null){
|
|
this.style.transform(matrix);
|
|
}
|
|
this.startPoint.transform(matrix);
|
|
this.controlPoint.transform(matrix);
|
|
this.endPoint.transform(matrix);
|
|
},
|
|
|
|
|
|
//TODO: dynamically adjust until the length of a segment is small enough
|
|
//(1 unit)?
|
|
getPoints:function(){
|
|
var STEP = 0.01;
|
|
var points = [];
|
|
for(var t = 0; t<=1; t+=STEP){
|
|
points.push(this.getPoint(t));
|
|
}
|
|
|
|
return points;
|
|
},
|
|
|
|
/**
|
|
* Return the point corresponding to parameter value t
|
|
* @param {Number} t the value of t parameter, t in [0,1]
|
|
* @return {Point} the value of t parameter, t in [0,1]
|
|
* @see http://html5tutorial.com/how-to-join-two-bezier-curves-with-the-canvas-api/
|
|
* */
|
|
getPoint:function(t){
|
|
var a = Math.pow((1 - t), 2);
|
|
var b = 2 * (1 - t) * t;
|
|
var c = Math.pow(t, 2);
|
|
var Xp = a * this.startPoint.x + b * this.controlPoint.x + c * this.endPoint.x;
|
|
var Yp = a * this.startPoint.y + b * this.controlPoint.y + c * this.endPoint.y;
|
|
|
|
return new Point(Xp, Yp);
|
|
},
|
|
|
|
/**Return the {Point} corresponding the t certain t value.
|
|
* NOTE: t is the visual percentage of a point from the start of the
|
|
* primitive and the result is
|
|
* different than the t from getPoint(...) method
|
|
* @param {Number} t the value of parameter t, where t in [0,1]*/
|
|
getVisualPoint:function (t){
|
|
var points = this.getPoints();
|
|
var polyline = new Polyline();
|
|
polyline.points = points;
|
|
|
|
return polyline.getVisualPoint(t);
|
|
},
|
|
|
|
|
|
|
|
/**Computes the length of the Cubic Curve
|
|
* TODO: This is by far not the best algorithm but only an aproximation.
|
|
* */
|
|
getLength:function(){
|
|
/*Algorithm: split the Bezier curve into an aproximative
|
|
polyline and use polyline's near method*/
|
|
var poly = new Polyline();
|
|
|
|
for(var t=0; t<=1; t+=0.01){
|
|
poly.addPoint(this.getPoint(t));
|
|
}
|
|
|
|
return poly.getLength();
|
|
},
|
|
|
|
|
|
/*We could use an interpolation algorightm t=0,1 and pick 10 points to iterate on ...but for now it's fine
|
|
**/
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
context.beginPath();
|
|
|
|
if(this.style!=null){
|
|
this.style.setupContext(context);
|
|
}
|
|
|
|
context.moveTo(this.startPoint.x, this.startPoint.y);
|
|
context.quadraticCurveTo(this.controlPoint.x, this.controlPoint.y, this.endPoint.x, this.endPoint.y);
|
|
|
|
//first fill
|
|
if(this.style.fillStyle!=null && this.style.fillStyle!=""){
|
|
context.fill();
|
|
}
|
|
|
|
//then stroke
|
|
if(this.style.strokeStyle!=null && this.style.strokeStyle!=""){
|
|
context.stroke();
|
|
}
|
|
|
|
|
|
if(false){ //structure polyline
|
|
var polyline = new Polyline();
|
|
polyline.style.strokeStyle = '#ccc';
|
|
polyline.points = this.getPoints();
|
|
polyline.paint(context);
|
|
context.stroke();
|
|
|
|
context.fillStyle = "#F00";
|
|
context.fillRect(this.startPoint.x-2, this.startPoint.y-2, 4, 4);
|
|
context.fillRect(this.controlPoint.x-2, this.controlPoint.y-2, 4, 4);
|
|
context.fillRect(this.endPoint.x-2, this.endPoint.y-2, 4, 4);
|
|
}
|
|
},
|
|
|
|
/*
|
|
*TODO: algorithm not clear and maybe we can find the math formula to determine if we have an intersection
|
|
*@see <a href="http://rosettacode.org/wiki/Bitmap/B%C3%A9zier_curves/Quadratic">http://rosettacode.org/wiki/Bitmap/B%C3%A9zier_curves/Quadratic</a>
|
|
*/
|
|
deprecated__near:function(x, y, radius){
|
|
var polls=100;
|
|
if(!Util.isPointInside(new Point(x,y), [this.startPoint, this.controlPoint, this.endPoint])
|
|
&& !this.startPoint.near(x,y,radius) && ! this.endPoint.near(x,y,radius)){
|
|
return false;//not inside the control points, so can't be near the line
|
|
}
|
|
var low=0;
|
|
var high=polls;
|
|
var i=(high-low)/2;
|
|
while(i >= low && i <= high && high-low>0.01){//high-low indicates>0.01 stops us from taking increasingly tiny steps
|
|
i=low+(high-low)/2 //we want the mid point
|
|
|
|
//don't fully understand this
|
|
var t = i / polls;
|
|
var fromEnd = Math.pow((1.0 - t), 2); //get how far from end we are and square it
|
|
var a = 2.0 * t * (1.0 - t);
|
|
var fromStart = Math.pow(t, 2); //get how far from start we are and square it
|
|
var newX = fromEnd * this.startPoint.x + a * this.controlPoint.x + this.fromStart * this.endPoint.x;//?
|
|
var newY = fromEnd * this.startPoint.y + a * this.controlPoint.y + this.fromStart * this.endPoint.y;//?
|
|
var p = new Point(newX,newY);
|
|
if(p.near(x, y, radius)){
|
|
return true;
|
|
}
|
|
|
|
//get distance between start and the point we are looking for, and the current point on line
|
|
var pToStart=Math.sqrt(Math.pow(this.startPoint.x-p.x,2)+Math.pow(this.startPoint.y-p.y,2));
|
|
var myToStart=Math.sqrt(Math.pow(this.startPoint.x-x,2)+Math.pow(this.startPoint.y-y,2));
|
|
|
|
//if our point is closer to start, we know that our cursor must be between start and where we are
|
|
if(myToStart<pToStart){
|
|
high=i;
|
|
}
|
|
else if(myToStart!=pToStart){
|
|
low=i;
|
|
}
|
|
else{
|
|
return false;//their distance is the same but the point is not near, return false.
|
|
}
|
|
return this.startPoint.near(x,y,radius)|| this.endPoint.near(x,y,radius);
|
|
}
|
|
},
|
|
|
|
near:function(x, y, radius){
|
|
var points = this.getPoints();
|
|
var polyline = new Polyline();
|
|
polyline.points = points;
|
|
return polyline.near(x, y, radius);
|
|
},
|
|
|
|
clone:function(){
|
|
var ret=new QuadCurve(this.startPoint.clone(),this.controlPoint.clone(),this.endPoint.clone());
|
|
ret.style=this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
equals:function(anotherQuadCurve){
|
|
if(!anotherQuadCurve instanceof QuadCurve){
|
|
return false;
|
|
}
|
|
|
|
return this.startPoint.equals(anotherQuadCurve.startPoint)
|
|
&& this.controlPoint.equals(anotherQuadCurve.controlPoint)
|
|
&& this.endPoint.equals(anotherQuadCurve.endPoint)
|
|
&& this.style.equals(anotherQuadCurve.style);
|
|
},
|
|
|
|
/**
|
|
*@deprecated
|
|
**/
|
|
deprecated_contains:function(x, y){
|
|
return this.near(x,y,3);
|
|
points=[this.startPoint,this.controlPoint,this.endPoint];
|
|
return Util.isPointInside(new Point(x,y),points);
|
|
},
|
|
|
|
/**
|
|
* @see sources for <a href="http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/awt/geom/QuadCurve2D.java">java.awt.geom.QuadCurve2D</a>
|
|
* @author (just converted to JavaScript) alex@scriptoid.com
|
|
*/
|
|
deprecated_2_contains:function(x,y) {
|
|
|
|
var x1 = this.startPoint.x;
|
|
var y1 = this.startPoint.y;
|
|
var xc = this.controlPoint.x;
|
|
var yc = this.controlPoint.y;
|
|
var x2 = this.endPoint.x;
|
|
var y2 = this.endPoint.y;
|
|
|
|
/*
|
|
* We have a convex shape bounded by quad curve Pc(t)
|
|
* and ine Pl(t).
|
|
*
|
|
* P1 = (x1, y1) - start point of curve
|
|
* P2 = (x2, y2) - end point of curve
|
|
* Pc = (xc, yc) - control point
|
|
*
|
|
* Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
|
|
* = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
|
|
* Pl(t) = P1*(1 - t) + P2*t
|
|
* t = [0:1]
|
|
*
|
|
* P = (x, y) - point of interest
|
|
*
|
|
* Let's look at second derivative of quad curve equation:
|
|
*
|
|
* Pq''(t) = 2 * (P1 - 2 * Pc + P2) = Pq''
|
|
* It's constant vector.
|
|
*
|
|
* Let's draw a line through P to be parallel to this
|
|
* vector and find the intersection of the quad curve
|
|
* and the line.
|
|
*
|
|
* Pq(t) is point of intersection if system of equations
|
|
* below has the solution.
|
|
*
|
|
* L(s) = P + Pq''*s == Pq(t)
|
|
* Pq''*s + (P - Pq(t)) == 0
|
|
*
|
|
* | xq''*s + (x - xq(t)) == 0
|
|
* | yq''*s + (y - yq(t)) == 0
|
|
*
|
|
* This system has the solution if rank of its matrix equals to 1.
|
|
* That is, determinant of the matrix should be zero.
|
|
*
|
|
* (y - yq(t))*xq'' == (x - xq(t))*yq''
|
|
*
|
|
* Let's solve this equation with 't' variable.
|
|
* Also let kx = x1 - 2*xc + x2
|
|
* ky = y1 - 2*yc + y2
|
|
*
|
|
* t0q = (1/2)*((x - x1)*ky - (y - y1)*kx) /
|
|
* ((xc - x1)*ky - (yc - y1)*kx)
|
|
*
|
|
* Let's do the same for our line Pl(t):
|
|
*
|
|
* t0l = ((x - x1)*ky - (y - y1)*kx) /
|
|
* ((x2 - x1)*ky - (y2 - y1)*kx)
|
|
*
|
|
* It's easy to check that t0q == t0l. This fact means
|
|
* we can compute t0 only one time.
|
|
*
|
|
* In case t0 < 0 or t0 > 1, we have an intersections outside
|
|
* of shape bounds. So, P is definitely out of shape.
|
|
*
|
|
* In case t0 is inside [0:1], we should calculate Pq(t0)
|
|
* and Pl(t0). We have three points for now, and all of them
|
|
* lie on one line. So, we just need to detect, is our point
|
|
* of interest between points of intersections or not.
|
|
*
|
|
* If the denominator in the t0q and t0l equations is
|
|
* zero, then the points must be collinear and so the
|
|
* curve is degenerate and encloses no area. Thus the
|
|
* result is false.
|
|
*/
|
|
var kx = x1 - 2 * xc + x2;
|
|
var ky = y1 - 2 * yc + y2;
|
|
var dx = x - x1;
|
|
var dy = y - y1;
|
|
var dxl = x2 - x1;
|
|
var dyl = y2 - y1;
|
|
|
|
var t0 = (dx * ky - dy * kx) / (dxl * ky - dyl * kx);
|
|
if (t0 < 0 || t0 > 1 || t0 != t0) {
|
|
return false;
|
|
}
|
|
|
|
var xb = kx * t0 * t0 + 2 * (xc - x1) * t0 + x1;
|
|
var yb = ky * t0 * t0 + 2 * (yc - y1) * t0 + y1;
|
|
var xl = dxl * t0 + x1;
|
|
var yl = dyl * t0 + y1;
|
|
|
|
return (x >= xb && x < xl) ||
|
|
(x >= xl && x < xb) ||
|
|
(y >= yb && y < yl) ||
|
|
(y >= yl && y < yb);
|
|
},
|
|
|
|
contains:function(x,y) {
|
|
var points = this.getPoints();
|
|
var polyline = new Polyline();
|
|
polyline.points = points;
|
|
return polyline.contains(x, y);
|
|
},
|
|
|
|
toString:function(){
|
|
return 'quad(' + this.startPoint + ',' + this.controlPoint + ',' + this.endPoint + ')';
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive
|
|
*@see <a href="http://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands">http://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands</a>
|
|
**/
|
|
toSVG:function(){
|
|
//<path d="M200,300 Q400,50 600,300 T1000,300" fill="none" stroke="red" stroke-width="5" />
|
|
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<path d="M';
|
|
result += this.startPoint.x + ',' + this.endPoint.y;
|
|
result += ' Q' + this.controlPoint.x + ',' + this.controlPoint.y;
|
|
result += ' ' + this.endPoint.x + ',' + this.endPoint.y;
|
|
|
|
result += '" '
|
|
+ this.style.toSVG()
|
|
+ ' />';
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates an instance of a cubic curve.
|
|
* A curved line determined by 2 normal points (startPoint and endPoint) and 2 control points (controlPoint1, controlPoint2)
|
|
*
|
|
* @constructor
|
|
* @this {CubicCurve}
|
|
* @param {Point} startPoint - starting point of the line
|
|
* @param {Point} controlPoint1 - 1st control point of the line
|
|
* @param {Point} controlPoint2 - 2nd control point of the line
|
|
* @param {Point} endPoint - the ending point of the line
|
|
* @see <a href="http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves">http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves</a>
|
|
**/
|
|
function CubicCurve(startPoint, controlPoint1, controlPoint2, endPoint){
|
|
/**The start {@link Point}*/
|
|
this.startPoint = startPoint;
|
|
|
|
/**The first controll {@link Point}*/
|
|
this.controlPoint1 = controlPoint1;
|
|
|
|
/**The second controll {@link Point}*/
|
|
this.controlPoint2 = controlPoint2;
|
|
|
|
/**The end {@link Point}*/
|
|
this.endPoint = endPoint;
|
|
|
|
/**The {@link Style} of the quad*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Object type used for JSON deserialization*/
|
|
this.oType = 'CubicCurve';
|
|
}
|
|
|
|
/**Creates a {CubicCurve} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {CubicCurve} a newly constructed CubicCurve
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
CubicCurve.load = function(o){
|
|
var newCubic = new CubicCurve(
|
|
Point.load(o.startPoint),
|
|
Point.load(o.controlPoint1),
|
|
Point.load(o.controlPoint2),
|
|
Point.load(o.endPoint)
|
|
);
|
|
|
|
newCubic.style = Style.load(o.style);
|
|
return newCubic;
|
|
}
|
|
|
|
|
|
CubicCurve.prototype = {
|
|
constructor : CubicCurve,
|
|
|
|
|
|
transform:function(matrix){
|
|
if(this.style != null){
|
|
this.style.transform(matrix);
|
|
}
|
|
this.startPoint.transform(matrix);
|
|
this.controlPoint1.transform(matrix);
|
|
this.controlPoint2.transform(matrix);
|
|
this.endPoint.transform(matrix);
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
context.beginPath();
|
|
|
|
// Log.group("CubicCurve:paint() ");
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
Log.info("stroke style : " + this.style.strokeStyle);
|
|
}
|
|
|
|
context.beginPath();
|
|
context.moveTo(this.startPoint.x, this.startPoint.y);
|
|
context.bezierCurveTo(
|
|
this.controlPoint1.x, this.controlPoint1.y,
|
|
this.controlPoint2.x, this.controlPoint2.y,
|
|
this.endPoint.x, this.endPoint.y
|
|
);
|
|
|
|
|
|
if(this.style.fillStyle != null && this.style.fillStyle != ""){
|
|
// Log.info("Fill provided");
|
|
context.fill();
|
|
}
|
|
else{
|
|
// Log.info("Fill NOT provided")
|
|
}
|
|
|
|
if(this.style.strokeStyle != null && this.style.strokeStyle != ""){
|
|
// Log.info("Stroke provided");
|
|
context.stroke();
|
|
}
|
|
else{
|
|
// Log.info("Stroke NOT provided")
|
|
}
|
|
|
|
Log.groupEnd();
|
|
},
|
|
|
|
|
|
clone:function(){
|
|
var ret = new CubicCurve(this.startPoint.clone(),this.controlPoint1.clone(), this.controlPoint2.clone(),this.endPoint.clone());
|
|
ret.style = this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
|
|
equals:function(anotherCubicCurve){
|
|
if(!anotherCubicCurve instanceof CubicCurve){
|
|
return false;
|
|
}
|
|
return this.startPoint.equals(anotherCubicCurve.startPoint)
|
|
&& this.controlPoint1.equals(anotherCubicCurve.controlPoint1)
|
|
&& this.controlPoint2.equals(anotherCubicCurve.controlPoint2)
|
|
&& this.endPoint.equals(anotherCubicCurve.endPoint);
|
|
},
|
|
|
|
/**
|
|
* Inspired by java.awt.geom.CubicCurve2D
|
|
* @author Alex
|
|
*/
|
|
contains:function(x, y) {
|
|
/*This piece of code is kept as a reference.
|
|
The "old" idea was to treat the curve as a polygon
|
|
thus closing it and apply a similar algorithm as for polygon.
|
|
Of course it does not offer precision :(
|
|
|
|
// if (!(x * 0.0 + y * 0.0 == 0.0)) {
|
|
// Either x or y was infinite or NaN.
|
|
// A NaN always produces a negative response to any test
|
|
// and Infinity values cannot be "inside" any path so
|
|
// they should return false as well.
|
|
//
|
|
return false;
|
|
}
|
|
// We count the "Y" crossings to determine if the point is
|
|
// inside the curve bounded by its closing line.
|
|
var x1 = this.startPoint.x //getX1();
|
|
var y1 = this.startPoint.y //getY1();
|
|
var x2 = this.endPoint.x //getX2();
|
|
var y2 = this.endPoint.y //getY2();
|
|
var crossings =
|
|
(Util.pointCrossingsForLine(x, y, x1, y1, x2, y2) +
|
|
Util.pointCrossingsForCubic(x, y,
|
|
x1, y1,
|
|
this.controlPoint1.x, this.controlPoint1.y,
|
|
this.controlPoint2.x, this.controlPoint2.y,
|
|
x2, y2, 0));
|
|
return ((crossings & 1) == 1);
|
|
*/
|
|
|
|
|
|
/*Algorithm: split the Bezier curve into an aproximative polyline and
|
|
* use {Polyline}'s contains(...) method
|
|
* */
|
|
var poly = new Polyline();
|
|
for(var t=0; t<=1; t=t+0.01){ //101 points :D
|
|
var a = Math.pow((1 - t), 3);
|
|
var b = 3 * t * Math.pow((1 - t), 2);
|
|
var c = 3 * Math.pow(t, 2) * (1 - t);
|
|
var d = Math.pow(t, 3);
|
|
var Xp = a * this.startPoint.x + b * this.controlPoint1.x + c * this.controlPoint2.x + d * this.endPoint.x;
|
|
var Yp = a * this.startPoint.y + b * this.controlPoint1.y + c * this.controlPoint2.y + d * this.endPoint.y;
|
|
poly.addPoint(new Point(Xp, Yp));
|
|
}
|
|
//Log.info("CubicCurve: Aproximative polyline " + poly);
|
|
|
|
return poly.contains(x, y);
|
|
},
|
|
|
|
|
|
/**
|
|
*Tests to see if a point is close enough to a cubic curve
|
|
*@param {Number} x - the x coordinates of the point
|
|
*@param {Number} y - the x coordinates of the point
|
|
*@param {Number} radius - the radius of vicinity
|
|
*@author alex
|
|
*@see <a href="http://rosettacode.org/wiki/Bitmap/B%C3%A9zier_curves/Cubic">http://rosettacode.org/wiki/Bitmap/B%C3%A9zier_curves/Cubic</a>
|
|
*/
|
|
near:function(x, y, radius){
|
|
|
|
/*Algorithm: split the Bezier curve into an aproximative
|
|
polyline and use polyline's near method*/
|
|
var poly = new Polyline();
|
|
|
|
for(var t=0; t<=1; t+=0.01){
|
|
var a = Math.pow((1 - t), 3);
|
|
var b = 3 * t * Math.pow((1 - t), 2);
|
|
var c = 3 * Math.pow(t, 2) * (1 - t);
|
|
var d = Math.pow(t, 3);
|
|
var Xp = a * this.startPoint.x + b * this.controlPoint1.x + c * this.controlPoint2.x + d * this.endPoint.x;
|
|
var Yp = a * this.startPoint.y + b * this.controlPoint1.y + c * this.controlPoint2.y + d * this.endPoint.y;
|
|
poly.addPoint(new Point(Xp, Yp));
|
|
}
|
|
|
|
return poly.near(x, y, radius);
|
|
},
|
|
|
|
//TODO: dynamically adjust until the length of a segment is small enough
|
|
//(1 unit)?
|
|
getPoints:function(){
|
|
var STEP = 0.01;
|
|
var points = [];
|
|
for(var t = 0; t<=1; t+=STEP){
|
|
points.push(this.getPoint(t));
|
|
}
|
|
|
|
return points;
|
|
},
|
|
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
/**Computes the length of the Cubic Curve
|
|
* TODO: This is by far not the best algorithm but only an aproximation.
|
|
* */
|
|
getLength:function(){
|
|
/*Algorithm: split the Bezier curve into an aproximative
|
|
polyline and use polyline's near method*/
|
|
var poly = new Polyline();
|
|
|
|
poly.points = this.getPoints();
|
|
|
|
return poly.getLength();
|
|
},
|
|
|
|
/**Return the {Point} corresponding a t value, from parametric equation of Curve
|
|
* @param {Number} t the value of parameter t, where t in [0,1]*/
|
|
getPoint: function(t){
|
|
var a = Math.pow((1 - t), 3);
|
|
var b = 3 * t * Math.pow((1 - t), 2);
|
|
var c = 3 * Math.pow(t, 2) * (1 - t);
|
|
var d = Math.pow(t, 3);
|
|
var Xp = a * this.startPoint.x + b * this.controlPoint1.x + c * this.controlPoint2.x + d * this.endPoint.x;
|
|
var Yp = a * this.startPoint.y + b * this.controlPoint1.y + c * this.controlPoint2.y + d * this.endPoint.y;
|
|
|
|
return new Point(Xp, Yp);
|
|
},
|
|
|
|
/**Return the {Point} corresponding the t certain t value.
|
|
* NOTE: t is the visual percentage of a point from the start of the
|
|
* primitive and the result is
|
|
* different than the t from getPoint(...) method
|
|
* @param {Number} t the value of parameter t, where t in [0,1]*/
|
|
getVisualPoint:function (t){
|
|
var points = this.getPoints();
|
|
var polyline = new Polyline();
|
|
polyline.points = points;
|
|
|
|
return polyline.getVisualPoint(t);
|
|
},
|
|
|
|
|
|
|
|
toString:function(){
|
|
return 'quad(' + this.startPoint + ',' + this.controlPoint1 + ',' + this.controlPoint2 + ',' + this.endPoint + ')';
|
|
},
|
|
|
|
/**Render the SVG fragment for this primitive
|
|
*@see <a href="http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands">http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands</a>
|
|
**/
|
|
toSVG:function(){
|
|
//<path d="M100,200 C100,100 250,100 250,200" />
|
|
|
|
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<path d="M';
|
|
result += this.startPoint.x + ',' + this.endPoint.y;
|
|
result += ' C' + this.controlPoint1.x + ',' + this.controlPoint1.y;
|
|
result += ' ' + this.controlPoint2.x + ',' + this.controlPoint2.y;
|
|
result += ' ' + this.endPoint.x + ',' + this.endPoint.y;
|
|
|
|
result += '" ' + this.style.toSVG() + ' />';
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws an arc.
|
|
* To allow easy transformations of the arc we will simulate the arc by a series of curves
|
|
*
|
|
* @constructor
|
|
* @this {Arc}
|
|
* @param {Number} x - the X coordinates of the "imaginary" circle of which the arc is part of
|
|
* @param {Number} y - the Y coordinates of the "imaginary" circle of which the arc is part of
|
|
* @param {Number} radius - the radius of the arc
|
|
* @param {Number} startAngle - the degrees (not radians as in Canvas'specs) of the starting angle for this arc
|
|
* @param {Number} endAngle - the degrees (not radians as in Canvas'specs) of the starting angle for this arc
|
|
* @param {Number} direction - the direction of drawing. For now it's from startAngle to endAngle (anticlockwise). Not really used
|
|
* @param {Number} styleFlag (optional) -
|
|
* 1: close path between start of arc and end,
|
|
* 2: draw pie slice, line to center point, line to start point
|
|
* default: empty/0/anything else: just draw the arc
|
|
* TODO: make it a class constant
|
|
* @see <a href="http://STACKoverflow.com/questions/2688808/drawing-quadratic-bezier-circles-with-a-given-radius-how-to-determine-control-po">http://STACKoverflow.com/questions/2688808/drawing-quadratic-bezier-circles-with-a-given-radius-how-to-determine-control-po</a>
|
|
**/
|
|
|
|
function Arc(x, y, radius, startAngle, endAngle, direction, styleFlag){
|
|
/**End angle. Required for dashedArc*/
|
|
this.endAngle = endAngle;
|
|
|
|
/**Start angle. required for dashedArc*/
|
|
this.startAngle = startAngle;
|
|
|
|
/**The center {@link Point} of the circle*/
|
|
this.middle = new Point(x,y);
|
|
|
|
/**The radius of the circle*/
|
|
this.radius = radius;
|
|
|
|
/**An {Array} of {@link QuadCurve}s used to draw the arc*/
|
|
this.curves = [];
|
|
|
|
/**Accuracy. It tells the story of how many QuadCurves we will use*/
|
|
var numControlPoints = 8;
|
|
|
|
/**The start {@link Point}*/
|
|
this.startPoint = null;
|
|
|
|
/**The end {@link Point}*/
|
|
this.endPoint = null;
|
|
|
|
/**The start angle, in radians*/
|
|
this.startAngleRadians = 0;
|
|
|
|
/**The end angle, in radians*/
|
|
this.endAngleRadians = 0;
|
|
|
|
//code shamelessly stollen from the above site.
|
|
var start = Math.PI/180 * startAngle; //convert the angles back to radians
|
|
this.startAngleRadians = start;
|
|
this.endAngleRadians = Math.PI/180 * endAngle;
|
|
var arcLength = (Math.PI/180*(endAngle-startAngle))/ numControlPoints;
|
|
for (var i = 0; i < numControlPoints; i++) {
|
|
if (i < 1)
|
|
{
|
|
this.startPoint = new Point(x + radius * Math.cos(arcLength * i),y + radius * Math.sin(arcLength * i))
|
|
}
|
|
var startPoint=new Point(x + radius * Math.cos(arcLength * i),y + radius * Math.sin(arcLength * i))
|
|
|
|
//control radius formula
|
|
//where does it come from, why does it work?
|
|
var controlRadius = radius / Math.cos(arcLength * .5);
|
|
|
|
//the control point is plotted halfway between the arcLength and uses the control radius
|
|
var controlPoint=new Point(x + controlRadius * Math.cos(arcLength * (i + 1) - arcLength * .5),y + controlRadius * Math.sin(arcLength * (i + 1) - arcLength * .5))
|
|
if (i == (numControlPoints - 1))
|
|
{
|
|
this.endPoint = new Point(x + radius * Math.cos(arcLength * (i + 1)),y + radius * Math.sin(arcLength * (i + 1)));
|
|
}
|
|
var endPoint=new Point(x + radius * Math.cos(arcLength * (i + 1)),y + radius * Math.sin(arcLength * (i + 1)));
|
|
|
|
|
|
//if we arent starting at 0, rotate it to where it needs to be
|
|
|
|
//move to origin (O)
|
|
startPoint.transform(Matrix.translationMatrix(-x,-y));
|
|
controlPoint.transform(Matrix.translationMatrix(-x,-y));
|
|
endPoint.transform(Matrix.translationMatrix(-x,-y));
|
|
|
|
//rotate by angle (start)
|
|
startPoint.transform(Matrix.rotationMatrix(start));
|
|
controlPoint.transform(Matrix.rotationMatrix(start));
|
|
endPoint.transform(Matrix.rotationMatrix(start));
|
|
|
|
//move it back to where it was
|
|
startPoint.transform(Matrix.translationMatrix(x,y));
|
|
controlPoint.transform(Matrix.translationMatrix(x,y));
|
|
endPoint.transform(Matrix.translationMatrix(x,y));
|
|
|
|
this.curves.push(new QuadCurve(startPoint,controlPoint,endPoint));
|
|
}
|
|
|
|
/**The style flag - see contructor's arguments*/
|
|
this.styleFlag = styleFlag;
|
|
|
|
/**The {@link Style} of the arc*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Adding a reference to the end point makes the transform code hugely cleaner*/
|
|
this.direction = direction;
|
|
|
|
/**Object type used for JSON deserialization*/
|
|
this.oType = 'Arc';
|
|
}
|
|
|
|
|
|
/**Creates a {Arc} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Arc} a newly constructed Arc
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Arc.load = function(o){
|
|
var newArc = new Arc();
|
|
|
|
newArc.endAngle = o.endAngle;
|
|
newArc.startAngle = o.startAngle;
|
|
newArc.middle = Point.load(o.middle);
|
|
newArc.radius = o.radius
|
|
newArc.curves = QuadCurve.loadArray(o.curves);
|
|
|
|
/*we need to load these 'computed' values as they are computed only in constructor :(
|
|
*TODO: maybe make a new function setUp() that deal with this*/
|
|
newArc.startPoint = Point.load(o.startPoint);
|
|
newArc.endPoint = Point.load(o.endPoint);
|
|
newArc.startAngleRadians = o.startAngleRadians;
|
|
newArc.endAngleRadians = o.endAngleRadians;
|
|
|
|
newArc.styleFlag = o.styleFlag;
|
|
newArc.style = Style.load(o.style);
|
|
newArc.direction = o.direction;
|
|
|
|
return newArc;
|
|
}
|
|
|
|
/**Creates a {Arc} out of an Array of JSON parsed object
|
|
*@param {Array} v - an {Array} of JSON parsed objects
|
|
*@return {Array}of newly constructed Arcs
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Arc.loadArray = function(v){
|
|
var newArcs = [];
|
|
|
|
for(var i=0; i<v.length; i++){
|
|
newArcs.push(Arc.load(v[i]));
|
|
}
|
|
|
|
return newArcs;
|
|
}
|
|
|
|
|
|
|
|
Arc.prototype = {
|
|
|
|
constructor : Arc,
|
|
|
|
transform:function(matrix){
|
|
/* rotations - ok
|
|
* translations - ok
|
|
* scale - ok
|
|
* skew - NOT ok (i do not know how to preserve points, angles...etc- maybe a Cola :)
|
|
**/
|
|
|
|
//transform the style
|
|
if(this.style != null){
|
|
this.style.transform(matrix);
|
|
}
|
|
|
|
//transform the center of the circle
|
|
this.middle.transform(matrix);
|
|
|
|
//transform each curve
|
|
for(var i=0; i<this.curves.length; i++){
|
|
this.curves[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
|
|
paint:function(context){
|
|
context.beginPath();
|
|
|
|
if(this.style!=null){
|
|
this.style.setupContext(context);
|
|
}
|
|
context.lineWidth = this.style.lineWidth;
|
|
//context.arc(x,y,radius,(Math.PI/180)*startAngle,(Math.PI/180)*endAngle,direction);
|
|
context.moveTo(this.curves[0].startPoint.x, this.curves[0].startPoint.y);
|
|
for(var i=0; i<this.curves.length; i++){
|
|
context.quadraticCurveTo(this.curves[i].controlPoint.x, this.curves[i].controlPoint.y
|
|
,this.curves[i].endPoint.x, this.curves[i].endPoint.y);
|
|
//curves[i].paint(context);
|
|
}
|
|
|
|
if(this.styleFlag == 1){
|
|
context.closePath();
|
|
}
|
|
else if(this.styleFlag == 2){
|
|
context.lineTo(this.middle.x, this.middle.y);
|
|
context.closePath();
|
|
}
|
|
|
|
//first fill
|
|
if(this.style.fillStyle!=null && this.style.fillStyle!=""){
|
|
context.fill();
|
|
}
|
|
|
|
//then stroke
|
|
if(this.style.strokeStyle!=null && this.style.strokeStyle!=""){
|
|
context.stroke();
|
|
}
|
|
|
|
},
|
|
|
|
clone:function(){
|
|
var ret = new Arc(this.middle.x, this.middle.y, this.radius, this.startAngle, this.endAngle, this.direction, this.styleFlag);
|
|
for (var i=0; i< this.curves.length; i++){
|
|
ret.curves[i]=this.curves[i].clone();
|
|
}
|
|
ret.style=this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
equals:function(anotherArc){
|
|
if(!anotherArc instanceof Arc){
|
|
return false;
|
|
}
|
|
|
|
//check curves
|
|
for(var i = 0 ; i < this.curves.lenght; i++){
|
|
if(!this.curves[i].equals(anotherArc.curves[i])){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return this.startAngle == anotherArc.startAngle
|
|
&& this.endAngle == anotherArc.endAngle
|
|
&& this.middle.equals(anotherArc.middle)
|
|
&& this.radius == anotherArc.radius
|
|
&& this.numControlPoints == anotherArc.numControlPoints
|
|
&& this.startPoint.equals(anotherArc.startPoint)
|
|
&& this.endPoint.equals(anotherArc.endPoint)
|
|
&& this.startAngleRadians == anotherArc.startAngleRadians
|
|
&& this.endAngleRadians == anotherArc.endAngleRadians
|
|
;
|
|
},
|
|
|
|
near:function(thex,they,theradius){
|
|
for(var i=0; i<this.curves.length; i++){
|
|
if(this.curves[i].near(thex,they,theradius)){
|
|
return true;
|
|
}
|
|
}
|
|
//return (distance && angle) || finishLine || startLine || new Point(x,y).near(thex,they,theradius);
|
|
|
|
return false;
|
|
},
|
|
|
|
contains: function(thex,they){
|
|
var p = this.getPoints();
|
|
return Util.isPointInside((new Point(thex,they)), p);
|
|
|
|
},
|
|
|
|
/**Get the points of the Arc
|
|
*@return {Array} of {Point}s
|
|
*@author Zack
|
|
**/
|
|
getPoints:function(){
|
|
var p = [];
|
|
if(this.styleFlag ==2){
|
|
p.push(this.middle);
|
|
}
|
|
for(var i=0; i<this.curves.length; i++){
|
|
var c = this.curves[i].getPoints();
|
|
for(var a=0; a<c.length; a++){
|
|
p.push(c[a]);
|
|
}
|
|
}
|
|
return p;
|
|
},
|
|
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
toString:function(){
|
|
return 'arc(' + new Point(this.x,this.y) + ',' + this.radius + ',' + this.startAngle + ',' + this.endAngle + ',' + this.direction + ')';
|
|
},
|
|
|
|
|
|
/**
|
|
*As we simulate an arc by {QuadCurve}s so we will collect all of them
|
|
*and add it to a <path/> element.
|
|
*Note: We might use the SVG's arc command but what if the arc got distorted by a transformation?
|
|
*@see <a href="http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands">http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands</a>
|
|
*@see <a href="http://tutorials.jenkov.com/svg/path-element.html">http://tutorials.jenkov.com/svg/path-element.html</a>
|
|
*@author Alex <alex@scriptoid.com>
|
|
**/
|
|
toSVG: function(){
|
|
var r = "\n" + repeat("\t", INDENTATION) + '<path d="';
|
|
r += ' M' + this.curves[0].startPoint.x + ',' + this.curves[0].startPoint.y
|
|
for(var i=0; i<this.curves.length; i++){
|
|
r += ' Q' + this.curves[i].controlPoint.x + ',' + this.curves[i].controlPoint.y
|
|
+ ' ' + this.curves[i].endPoint.x + ',' + this.curves[i].endPoint.y;
|
|
}
|
|
r += '" ';
|
|
r += this.style.toSVG();
|
|
r += '/>';
|
|
return r;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Approximate an ellipse through 4 bezier curves, one for each quadrant
|
|
*
|
|
* @constructor
|
|
* @this {Ellipse}
|
|
* @param {Point} centerPoint - the center point of the ellipse
|
|
* @param {Number} width - the width of the ellipse
|
|
* @param {Number} height - the height of the ellipse
|
|
|
|
* @see <a href="http://www.codeguru.com/cpp/g-m/gdi/article.php/c131">http://www.codeguru.com/cpp/g-m/gdi/article.php/c131</a>
|
|
* @see <a href="http://www.tinaja.com/glib/ellipse4.pdf">http://www.tinaja.com/glib/ellipse4.pdf</a>
|
|
* @author Zack Newsham <zack_newsham@yahoo.co.uk>
|
|
**/
|
|
function Ellipse(centerPoint, width, height) {
|
|
/**"THE" constant*/
|
|
var EToBConst = 0.2761423749154;
|
|
|
|
/**Width offset*/
|
|
var offsetWidth = width * 2 * EToBConst;
|
|
|
|
/**Height offset*/
|
|
var offsetHeight = height * 2 * EToBConst;
|
|
|
|
/**The center {@link Point}*/
|
|
this.centerPoint = centerPoint;
|
|
|
|
/**Top left {@link CubicCurve}*/
|
|
this.topLeftCurve = new CubicCurve(new Point(centerPoint.x-width,centerPoint.y),new Point(centerPoint.x-width,centerPoint.y-offsetHeight),new Point(centerPoint.x-offsetWidth,centerPoint.y-height),new Point(centerPoint.x,centerPoint.y-height));
|
|
|
|
/**Top right {@link CubicCurve}*/
|
|
this.topRightCurve = new CubicCurve(new Point(centerPoint.x,centerPoint.y-height),new Point(centerPoint.x+offsetWidth,centerPoint.y-height),new Point(centerPoint.x+width,centerPoint.y-offsetHeight),new Point(centerPoint.x+width,centerPoint.y));
|
|
|
|
/**Bottom right {@link CubicCurve}*/
|
|
this.bottomRightCurve = new CubicCurve(new Point(centerPoint.x+width,centerPoint.y),new Point(centerPoint.x+width,centerPoint.y+offsetHeight),new Point(centerPoint.x+offsetWidth,centerPoint.y+height),new Point(centerPoint.x,centerPoint.y+height));
|
|
|
|
/**Bottom left {@link CubicCurve}*/
|
|
this.bottomLeftCurve = new CubicCurve(new Point(centerPoint.x,centerPoint.y+height),new Point(centerPoint.x-offsetWidth,centerPoint.y+height),new Point(centerPoint.x-width,centerPoint.y+offsetHeight),new Point(centerPoint.x-width,centerPoint.y));
|
|
|
|
/**The matrix array*/
|
|
this.matrix = null; //TODO: do we really need this?
|
|
|
|
/**The {@link Style} used*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Oject type used for JSON deserialization*/
|
|
this.oType = 'Ellipse';
|
|
}
|
|
|
|
/**Creates a new {Ellipse} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Ellipse} a newly constructed Ellipse
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Ellipse.load = function(o){
|
|
var newEllipse= new Ellipse(new Point(0,0), 0, 0); //fake ellipse (if we use a null centerPoint we got errors)
|
|
|
|
newEllipse.offsetWidth = o.offsetWidth;
|
|
newEllipse.offsetHeight = o.offsetHeight;
|
|
newEllipse.centerPoint = Point.load(o.centerPoint);
|
|
newEllipse.topLeftCurve = CubicCurve.load(o.topLeftCurve);
|
|
newEllipse.topRightCurve = CubicCurve.load(o.topRightCurve);
|
|
newEllipse.bottomRightCurve = CubicCurve.load(o.bottomRightCurve);
|
|
newEllipse.bottomLeftCurve = CubicCurve.load(o.bottomLeftCurve);
|
|
this.matrix = Matrix.clone(o.matrix);
|
|
newEllipse.style = Style.load(o.style);
|
|
|
|
return newEllipse;
|
|
}
|
|
|
|
|
|
Ellipse.prototype = {
|
|
constructor: Ellipse,
|
|
|
|
transform:function(matrix){
|
|
this.topLeftCurve.transform(matrix);
|
|
this.topRightCurve.transform(matrix);
|
|
this.bottomLeftCurve.transform(matrix);
|
|
this.bottomRightCurve.transform(matrix);
|
|
this.centerPoint.transform(matrix);
|
|
if(this.style){
|
|
this.style.transform(matrix);
|
|
}
|
|
},
|
|
|
|
paint:function(context){
|
|
if(this.style!=null){
|
|
this.style.setupContext(context);
|
|
}
|
|
context.beginPath();
|
|
context.moveTo(this.topLeftCurve.startPoint.x, this.topLeftCurve.startPoint.y);
|
|
context.bezierCurveTo(this.topLeftCurve.controlPoint1.x,
|
|
this.topLeftCurve.controlPoint1.y, this.topLeftCurve.controlPoint2.x,
|
|
this.topLeftCurve.controlPoint2.y, this.topLeftCurve.endPoint.x,
|
|
this.topLeftCurve.endPoint.y);
|
|
context.bezierCurveTo(this.topRightCurve.controlPoint1.x,
|
|
this.topRightCurve.controlPoint1.y, this.topRightCurve.controlPoint2.x,
|
|
this.topRightCurve.controlPoint2.y, this.topRightCurve.endPoint.x,
|
|
this.topRightCurve.endPoint.y);
|
|
context.bezierCurveTo(this.bottomRightCurve.controlPoint1.x,
|
|
this.bottomRightCurve.controlPoint1.y, this.bottomRightCurve.controlPoint2.x,
|
|
this.bottomRightCurve.controlPoint2.y, this.bottomRightCurve.endPoint.x,
|
|
this.bottomRightCurve.endPoint.y);
|
|
context.bezierCurveTo(this.bottomLeftCurve.controlPoint1.x,
|
|
this.bottomLeftCurve.controlPoint1.y, this.bottomLeftCurve.controlPoint2.x,
|
|
this.bottomLeftCurve.controlPoint2.y, this.bottomLeftCurve.endPoint.x,
|
|
this.bottomLeftCurve.endPoint.y);
|
|
|
|
//first fill
|
|
if(this.style.fillStyle!=null && this.style.fillStyle!=""){
|
|
context.fill();
|
|
}
|
|
|
|
//then stroke
|
|
if(this.style.strokeStyle!=null && this.style.strokeStyle!=""){
|
|
context.stroke();
|
|
}
|
|
|
|
},
|
|
|
|
contains:function(x,y){
|
|
var points = this.topLeftCurve.getPoints();
|
|
var curves = [this.topRightCurve, this.bottomRightCurve, this.bottomLeftCurve];
|
|
for(var i=0; i<curves.length; i++){
|
|
var curPoints = curves[i].getPoints();
|
|
|
|
for(var a=0; a<curPoints.length; a++){
|
|
points.push(curPoints[a]);
|
|
}
|
|
}
|
|
return Util.isPointInside(new Point(x,y), points);
|
|
|
|
return false;
|
|
},
|
|
|
|
near:function(x,y,radius){
|
|
return this.topLeftCurve.near(x,y,radius) || this.topRightCurve.near(x,y,radius) || this.bottomLeftCurve.near(x,y,radius) || this.bottomRightCurve.near(x,y,radius);
|
|
},
|
|
|
|
equals:function(anotherEllipse){
|
|
if(!anotherEllipse instanceof Ellipse){
|
|
return false;
|
|
}
|
|
|
|
return this.offsetWidth == anotherEllipse.offsetWidth
|
|
&& this.offsetHeight == anotherEllipse.offsetHeight
|
|
&& this.centerPoint.equals(anotherEllipse.centerPoint)
|
|
&& this.topLeftCurve.equals(anotherEllipse.topLeftCurve)
|
|
&& this.topRightCurve.equals(anotherEllipse.topRightCurve)
|
|
&& this.bottomRightCurve.equals(anotherEllipse.bottomRightCurve)
|
|
&& this.bottomLeftCurve.equals(anotherEllipse.bottomLeftCurve);
|
|
//TODO: add this && this.matrix.equals(anotherEllipse.bottomLeftCurve)
|
|
//TODO: add this && this.style.equals(anotherEllipse.bottomLeftCurve)
|
|
},
|
|
|
|
clone:function(){
|
|
var ret=new Ellipse(this.centerPoint.clone(),10,10);
|
|
ret.topLeftCurve=this.topLeftCurve.clone();
|
|
ret.topRightCurve=this.topRightCurve.clone();
|
|
ret.bottomLeftCurve=this.bottomLeftCurve.clone();
|
|
ret.bottomRightCurve=this.bottomRightCurve.clone();
|
|
ret.style=this.style.clone();
|
|
return ret;
|
|
},
|
|
|
|
toString:function(){
|
|
return 'ellipse('+this.centerPoint+","+this.xRadius+","+this.yRadius+")";
|
|
},
|
|
|
|
/**
|
|
*@see <a href="http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands">http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands</a>
|
|
*@author Alex Gheorghiu <scriptoid.com>
|
|
**/
|
|
toSVG: function(){
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<path d="M';
|
|
result += this.topLeftCurve.startPoint.x + ',' + this.topLeftCurve.startPoint.y;
|
|
|
|
//top left curve
|
|
result += ' C' + this.topLeftCurve.controlPoint1.x + ',' + this.topLeftCurve.controlPoint1.y;
|
|
result += ' ' + this.topLeftCurve.controlPoint2.x + ',' + this.topLeftCurve.controlPoint2.y;
|
|
result += ' ' + this.topLeftCurve.endPoint.x + ',' + this.topLeftCurve.endPoint.y;
|
|
|
|
//top right curve
|
|
result += ' C' + this.topRightCurve.controlPoint1.x + ',' + this.topRightCurve.controlPoint1.y;
|
|
result += ' ' + this.topRightCurve.controlPoint2.x + ',' + this.topRightCurve.controlPoint2.y;
|
|
result += ' ' + this.topRightCurve.endPoint.x + ',' + this.topRightCurve.endPoint.y;
|
|
|
|
//bottom right curve
|
|
result += ' C' + this.bottomRightCurve.controlPoint1.x + ',' + this.bottomRightCurve.controlPoint1.y;
|
|
result += ' ' + this.bottomRightCurve.controlPoint2.x + ',' + this.bottomRightCurve.controlPoint2.y;
|
|
result += ' ' + this.bottomRightCurve.endPoint.x + ',' + this.bottomRightCurve.endPoint.y;
|
|
|
|
//bottom left curve
|
|
result += ' C' + this.bottomLeftCurve.controlPoint1.x + ',' + this.bottomLeftCurve.controlPoint1.y;
|
|
result += ' ' + this.bottomLeftCurve.controlPoint2.x + ',' + this.bottomLeftCurve.controlPoint2.y;
|
|
result += ' ' + this.bottomLeftCurve.endPoint.x + ',' + this.bottomLeftCurve.endPoint.y;
|
|
|
|
result += '" ' + this.style.toSVG() + ' />';
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
getPoints:function(){
|
|
var points = [];
|
|
var curves = [this.topLeftCurve, this.topRightCurve,this.bottomRightCurve,this.bottomLeftCurve];
|
|
|
|
for(var i=0; i<curves.length; i++){
|
|
var curPoints = curves[i].getPoints();
|
|
for(var a=0; a<curPoints.length; a++){
|
|
points.push(curPoints[a]);
|
|
}
|
|
}
|
|
return points;
|
|
},
|
|
|
|
|
|
getBounds:function(){
|
|
return Util.getBounds(this.getPoints());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Approximate an ellipse through 4 bezier curves, one for each quadrant
|
|
*
|
|
* @constructor
|
|
* @this {DashedArc}
|
|
* @param {Number} x - x coodinated of the center of the "invisible" circle
|
|
* @param {Number} y - y coodinated of the center of the "invisible" circle
|
|
* @param {Number} radius - the radius of the "invisible" circle
|
|
* @param {Number} startAngle - the angle the arc will start from
|
|
* @param {Number} endAngle - the angle the arc will end into
|
|
* @param {Number} direction - direction of drawing (clockwise or anti-clock wise)
|
|
* @param {Style} styleFlag - the style of the arc
|
|
* @param {Number} dashGap - how big the gap between the lines will be
|
|
|
|
* @see <a href="http://www.codeguru.com/cpp/g-m/gdi/article.php/c131">http://www.codeguru.com/cpp/g-m/gdi/article.php/c131</a>
|
|
* @see <a href="http://www.tinaja.com/glib/ellipse4.pdf">http://www.tinaja.com/glib/ellipse4.pdf</a>
|
|
* @author Zack Newsham <zack_newsham@yahoo.co.uk>
|
|
**/
|
|
function DashedArc(x, y, radius, startAngle, endAngle, direction, styleFlag, dashGap){
|
|
/**The "under the hood" {@link Arc}*/
|
|
this.arc = new Arc(x, y, radius, startAngle, endAngle, direction, styleFlag);
|
|
|
|
/*The {@link Style} used**/
|
|
this.style = this.arc.style;
|
|
|
|
/**The gap between dashes*/
|
|
this.dashWidth = dashGap;
|
|
|
|
/**An {Array} or {@link Arc}s*/
|
|
this.lines = []; //an {Array} of {Arc}s
|
|
|
|
//init the parts
|
|
for(var i=0; i<100; i += this.dashWidth){
|
|
var a = new Arc(x, y, radius+this.style.lineWidth/2, (endAngle-startAngle)/100*i, (endAngle-startAngle)/100*(i+1), false);
|
|
a.style.strokeStyle = this.style.strokeStyle;
|
|
this.lines.push(a);
|
|
}
|
|
|
|
/**Object type used for JSON deserialization*/
|
|
this.oType = 'DashedArc';
|
|
}
|
|
|
|
|
|
/**Creates a new {Ellipse} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {DashedArc} a newly constructed DashedArc
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
DashedArc.load = function(o){
|
|
var newDashedArc = new DashedArc(100,100,30,0,360,false,0,6); //fake dashed (if we do not use it we got errors - endless loop)
|
|
newDashedArc.style.fillStyle="#ffffff"
|
|
|
|
newDashedArc.arc = Arc.load(o.arc);
|
|
newDashedArc.style = newDashedArc.arc.style; //strange but...
|
|
newDashedArc.dashWidth = o.dashWidth;
|
|
newDashedArc.lines = Arc.loadArray(o.lines);
|
|
|
|
|
|
return newDashedArc;
|
|
}
|
|
|
|
|
|
DashedArc.prototype = {
|
|
constructor: DashedArc,
|
|
|
|
transform:function(matrix){
|
|
this.arc.transform(matrix);
|
|
for (var i=0; i<this.lines.length; i++){
|
|
this.lines[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
getBounds:function(){
|
|
return this.arc.getBounds();
|
|
},
|
|
|
|
getPoints:function(){
|
|
return this.arc.getPoints();
|
|
},
|
|
|
|
contains:function(x,y){
|
|
return this.arc.contains(x,y);
|
|
},
|
|
|
|
near:function(x,y,radius){
|
|
return this.arc.near(x,y,radius);
|
|
},
|
|
|
|
|
|
toString:function(){
|
|
return this.arc.toString();
|
|
},
|
|
|
|
|
|
toSVG: function(){
|
|
throw 'Arc:toSVG() - not implemented';
|
|
},
|
|
|
|
/***/
|
|
equals:function(anotherDashedArc){
|
|
if(!anotherDashedArc instanceof DashedArc){
|
|
return false;
|
|
}
|
|
|
|
|
|
if(this.lines.length != anotherDashedArc.lines.length){
|
|
return false;
|
|
}
|
|
else{
|
|
for(var i in this.lines){
|
|
if(!this.lines[i].equals(anotherDashedArc.lines[i])){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.arc.equals(anotherDashedArc.arc)
|
|
&& this.style.equals(anotherDashedArc.style)
|
|
&& this.dashWidth == anotherDashedArc.dashWidth;
|
|
},
|
|
|
|
clone:function(){
|
|
return this.arc.clone();
|
|
},
|
|
|
|
paint:function(context){
|
|
this.style.setupContext(context);
|
|
context.lineCap="round"//this.style.lineCap;
|
|
for(var i=0; i<this.lines.length; i++){
|
|
context.beginPath();
|
|
this.lines[i].paint(context);
|
|
context.stroke();
|
|
}
|
|
this.style.strokeStyle=null;
|
|
this.arc.paint(context)
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* A path contains a number of elements (like shape) but they are drawn as one, i.e.
|
|
*
|
|
* @example
|
|
* begin path
|
|
* loop to draw shapes
|
|
* draw shape
|
|
* close loop
|
|
* close path
|
|
*
|
|
*
|
|
* @constructor
|
|
* @this {Path}
|
|
**/
|
|
function Path() {
|
|
/**An {Array} that will store all the basic primitives: {@link Point}s, {@link Line}s, {@link CubicCurve}s, etc that make the path*/
|
|
this.primitives = [];
|
|
|
|
/**The {@link Style} used for drawing*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Object type used for JSON deserialization*/
|
|
this.oType = 'Path';
|
|
}
|
|
|
|
/**Creates a new {Path} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Path} a newly constructed {Path}
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Path.load = function(o){
|
|
var newPath = new Path(); //fake path
|
|
|
|
newPath.style = Style.load(o.style);
|
|
|
|
for(var i=0; i< o.primitives.length; i++){
|
|
/**We can not use instanceof Point construction as
|
|
*the JSON objects are typeless... so JSONObject are simply objects */
|
|
if(o.primitives[i].oType == 'Point'){
|
|
newPath.primitives.push(Point.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Line'){
|
|
newPath.primitives.push(Line.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Polyline'){
|
|
newPath.primitives.push(Polyline.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Polygon'){
|
|
newPath.primitives.push(Polygon.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'QuadCurve'){
|
|
newPath.primitives.push(QuadCurve.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'CubicCurve'){
|
|
newPath.primitives.push(CubicCurve.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Arc'){
|
|
newPath.primitives.push(Arc.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Ellipse'){
|
|
newPath.primitives.push(Ellipse.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'DashedArc'){
|
|
newPath.primitives.push(DashedArc.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Path'){
|
|
newPath.primitives.push(Path.load(o.primitives[i]))
|
|
}
|
|
|
|
}
|
|
|
|
return newPath;
|
|
}
|
|
|
|
|
|
Path.prototype = {
|
|
constructor : Path,
|
|
|
|
transform:function(matrix){
|
|
for(var i = 0; i<this.primitives.length; i++ ){
|
|
this.primitives[i].transform(matrix);
|
|
}
|
|
},
|
|
|
|
addPrimitive:function(primitive){
|
|
this.primitives.push(primitive);
|
|
|
|
// update bound coordinates for gradient
|
|
this.style.gradientBounds = this.getBounds();
|
|
},
|
|
|
|
contains: function(x,y){
|
|
var points = [];
|
|
for(var i=0; i<this.primitives.length; i++){
|
|
if(this.primitives[i].contains(x,y)){
|
|
return true;
|
|
}
|
|
var curPoints = this.primitives[i].getPoints();
|
|
for(var a=0; a<curPoints.length; a++){
|
|
points.push(curPoints[a]);
|
|
}
|
|
}
|
|
return Util.isPointInside(new Point(x,y),points);
|
|
},
|
|
|
|
near: function(x,y,radius){
|
|
var points = [];
|
|
for(var i=0; i<this.primitives.length; i++){
|
|
if(this.primitives[i].near(x,y,radius)){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
getPoints:function(){
|
|
var points = [];
|
|
for (var i=0; i<this.primitives.length; i++){
|
|
points = points.concat(this.primitives[i].getPoints());
|
|
}
|
|
return points;
|
|
},
|
|
getBounds:function(){
|
|
var points = [];
|
|
for (var i in this.primitives) {
|
|
var bounds = this.primitives[i].getBounds();
|
|
points.push(new Point(bounds[0], bounds[1]));
|
|
points.push(new Point(bounds[2], bounds[3]));
|
|
}
|
|
return Util.getBounds(points);
|
|
},
|
|
|
|
clone:function(){
|
|
var ret = new Path();
|
|
for (var i=0; i<this.primitives.length; i++){
|
|
ret.addPrimitive(this.primitives[i].clone());
|
|
if(this.primitives[i].parentFigure){
|
|
ret.primitives[i].parentFigure=ret;
|
|
}
|
|
}
|
|
ret.style=this.style
|
|
return ret;
|
|
},
|
|
|
|
/**@author: Alex Gheorghiu <alex@scriptoid.com>*/
|
|
equals : function(anotherPath){
|
|
if(!anotherPath instanceof Path){
|
|
return false;
|
|
}
|
|
|
|
for(var i=0; i<this.primitives.length; i++){
|
|
if(!this.primitives[i].equals(anotherPath.primitives[i])){
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
paint:function(context){
|
|
context.save();
|
|
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
|
|
//PAINT FILL
|
|
//this loop is the one for the fill, we keep the reference.
|
|
//if you try to put these two together, you will get a line that is the same colour,
|
|
//even if you define different colours for each part of the line (i.e. fig 19)
|
|
//not allowing multiple colours in a single path will clean this code up hugely.
|
|
//
|
|
if(this.style.fillStyle != null && this.style.fillStyle != "" ){
|
|
context.beginPath();
|
|
context.moveTo(this.primitives[0].startPoint.x,this.primitives[0].startPoint.y);
|
|
for(var i = 0; i<this.primitives.length; i++ ){
|
|
var primitive = this.primitives[i];
|
|
// if(primitive instanceof Line){
|
|
// context.lineTo(primitive.endPoint.x,primitive.endPoint.y);
|
|
// }
|
|
// else if(primitive instanceof Polyline){
|
|
// for(var a=0; a<primitive.points.length; a++){
|
|
// context.lineTo(primitive.points[a].x,primitive.points[a].y);
|
|
// }
|
|
// }
|
|
// else if(primitive instanceof QuadCurve){
|
|
// context.quadraticCurveTo(primitive.controlPoint.x, primitive.controlPoint.y, primitive.endPoint.x, primitive.endPoint.y);
|
|
// }
|
|
// else if(primitive instanceof CubicCurve){
|
|
// context.bezierCurveTo(primitive.controlPoint1.x, primitive.controlPoint1.y, primitive.controlPoint2.x, primitive.controlPoint2.y, primitive.endPoint.x, primitive.endPoint.y)
|
|
// }
|
|
}
|
|
context.fill();
|
|
}
|
|
|
|
//PAINT STROKE
|
|
//This loop draws the lines of each individual shape. Each part might have a different strokeStyle !
|
|
if(this.style.strokeStyle != null && this.style.strokeStyle != "" ){
|
|
for(var i = 0; i<this.primitives.length; i++ ){
|
|
var primitive = this.primitives[i];
|
|
|
|
context.save();
|
|
context.beginPath();
|
|
|
|
//TODO: what if a primitive does not have a start point?
|
|
context.moveTo(primitive.startPoint.x,primitive.startPoint.y);
|
|
|
|
if(primitive instanceof Line){
|
|
context.lineTo(primitive.endPoint.x,primitive.endPoint.y);
|
|
context.closePath(); // added for line's correct Chrome's displaying
|
|
//Log.info("line");
|
|
}
|
|
else if(primitive instanceof Polyline){
|
|
for(var a=0; a<primitive.points.length; a++){
|
|
context.lineTo(primitive.points[a].x,primitive.points[a].y);
|
|
//Log.info("polyline");
|
|
}
|
|
}
|
|
else if(primitive instanceof QuadCurve){
|
|
context.quadraticCurveTo(primitive.controlPoint.x, primitive.controlPoint.y, primitive.endPoint.x, primitive.endPoint.y);
|
|
//Log.info("quadcurve");
|
|
}
|
|
else if(primitive instanceof CubicCurve){
|
|
context.bezierCurveTo(primitive.controlPoint1.x, primitive.controlPoint1.y, primitive.controlPoint2.x, primitive.controlPoint2.y, primitive.endPoint.x, primitive.endPoint.y)
|
|
//Log.info("cubiccurve");
|
|
}
|
|
else if(primitive instanceof Arc){
|
|
context.arc(primitive.startPoint.x, primitive.startPoint.y, primitive.radius, primitive.startAngleRadians, primitive.endAngleRadians, true)
|
|
//Log.info("arc" + primitive.startPoint.x + " " + primitive.startPoint.y);
|
|
}
|
|
else
|
|
{
|
|
//Log.info("unknown primitive");
|
|
}
|
|
|
|
//save primitive's old style
|
|
var oldStyle = primitive.style.clone();
|
|
|
|
//update primitive's style
|
|
if(primitive.style == null){
|
|
primitive.style = this.style;
|
|
}
|
|
else{
|
|
primitive.style.merge(this.style);
|
|
}
|
|
|
|
//use primitive's style
|
|
primitive.style.setupContext(context);
|
|
|
|
//stroke it
|
|
context.stroke();
|
|
|
|
//change primitive' style back to original one
|
|
primitive.style = oldStyle;
|
|
|
|
context.restore();
|
|
}
|
|
}
|
|
|
|
context.restore();
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
*Export this path to SVG
|
|
*@see <a href="http://tutorials.jenkov.com/svg/path-element.html">http://tutorials.jenkov.com/svg/path-element.html</a>
|
|
*@example
|
|
* <path d="M50,50
|
|
* A30,30 0 0,1 35,20
|
|
* L100,100
|
|
* M110,110
|
|
* L100,0"
|
|
* style="stroke:#660000; fill:none;"/>
|
|
*/
|
|
toSVG: function(){
|
|
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<path d="';
|
|
var previousPrimitive = null;
|
|
for(var i=0; i<this.primitives.length; i++){
|
|
|
|
var primitive = this.primitives[i];
|
|
|
|
if(primitive instanceof Point){
|
|
//TODO: implement me. Should we allow points?
|
|
/**Here is a big problem as if we implement the point as a line with 1px width
|
|
*upon scaling it will became obvious it's a line.
|
|
*Alternative solutions:
|
|
*1 - draw it as a SVG arc
|
|
*2 - draw it as an independent SVG circle*/
|
|
throw 'Path:toSVG()->Point - not implemented';
|
|
}
|
|
if(primitive instanceof Line){
|
|
/*If you want the Path to be a continuous contour we check if
|
|
*the M is unecessary - maybe we are already comming from that spot*/
|
|
if(previousPrimitive == null || previousPrimitive.endPoint.x != primitive.startPoint.x || previousPrimitive.endPoint.y != primitive.startPoint.y){
|
|
result += ' M' + primitive.startPoint.x + ',' + primitive.startPoint.y;
|
|
}
|
|
result += ' L' + primitive.endPoint.x + ',' + primitive.endPoint.y;
|
|
}
|
|
else if(primitive instanceof Polyline){
|
|
for(var a=0; a<primitive.points.length; a++){
|
|
result += ' L' + primitive.points[a].x + ',' + primitive.points[a].y;
|
|
//Log.info("polyline");
|
|
}
|
|
}
|
|
else if(primitive instanceof QuadCurve){
|
|
result += ' Q' + primitive.controlPoint.x + ',' + primitive.controlPoint.y + ',' + primitive.endPoint.x + ',' + primitive.endPoint.y ;
|
|
}
|
|
else if(primitive instanceof CubicCurve){
|
|
result += ' C' + primitive.controlPoint1.x + ',' + primitive.controlPoint1.y + ',' + primitive.controlPoint2.x + ',' + primitive.controlPoint2.y + ',' + primitive.endPoint.x + ',' + primitive.endPoint.y;
|
|
}
|
|
else if(primitive instanceof Arc){
|
|
//TODO: implement me
|
|
//<path d="M100,100 A25 25 0 0 0 150 100" stroke="lightgreen" stroke-width="4" fill="none" />
|
|
throw 'Path:toSVG()->Arc - not implemented';
|
|
}
|
|
else if(primitive instanceof Polyline){
|
|
//TODO: implement me
|
|
throw 'Path:toSVGPolylineArc - not implemented';
|
|
}
|
|
else{
|
|
throw 'Path:toSVG()->unknown primitive rendering not implemented';
|
|
}
|
|
|
|
previousPrimitive = primitive;
|
|
}//end for
|
|
result += '" '; //end of primitive shapes
|
|
// result += ' fill="none" stroke="#0F0F00" /> '; //end of path
|
|
result += this.style.toSVG(); //end of path
|
|
result += ' />'; //end of path
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**A figure is simply a collection of basic primitives: Points, Lines, Arcs, Curves and Paths
|
|
* A figure should not care about grouping primitives into Paths, each shape should draw itself.
|
|
* The Figure only delegate the painting to the composing shape.
|
|
*
|
|
* @constructor
|
|
* @this {Figure}
|
|
* @param {String} name - the name of the figure
|
|
*
|
|
**/
|
|
function Figure(name) {
|
|
/**Each Figure will have an unique Id on canvas*/
|
|
this.id = STACK.generateId();
|
|
|
|
/**Figure's name*/
|
|
this.name = name;
|
|
|
|
/**An {Array} of primitives that make the figure*/
|
|
this.primitives = [];
|
|
|
|
/**the Group'id to which this figure belongs to*/
|
|
this.groupId = -1;
|
|
|
|
/*Keeps track of all the handles for a figure*/
|
|
//this.handles = [];
|
|
//this.handleSelectedIndex=-1;
|
|
//this.builder = new Builder(this.id);
|
|
|
|
/**An {Array} of {@link BuilderProperty} objects*/
|
|
this.properties = [];
|
|
|
|
/**The {@link Style} use to draw this figure*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**We keep the figure position by having different points
|
|
*[central point of the figure, the middle of upper edge]
|
|
* An {Array} or {@link Point}s
|
|
**/
|
|
this.rotationCoords = [];
|
|
|
|
|
|
/**A {String} that point to a location*/
|
|
this.url = '';
|
|
|
|
/**Object type used for JSON deserialization*/
|
|
this.oType = 'Figure';
|
|
/**Object CCForm_Shape used for ccflow */
|
|
this.CCForm_Shape = null;
|
|
/**Object CCForm_MyPK used for ccflow */
|
|
this.CCForm_MyPK = null;
|
|
}
|
|
|
|
/**Creates a new {Figure} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {Figure} a newly constructed Figure
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
Figure.load = function(o){
|
|
var newFigure = new Figure(); //fake dashed (if we do not use it we got errors - endless loop)
|
|
|
|
newFigure.id = o.id;
|
|
newFigure.name = o.name;
|
|
for(var i=0; i< o.primitives.length; i++){
|
|
/**We can not use instanceof Point construction as
|
|
*the JSON objects are typeless... so JSONObject are simply objects */
|
|
if(o.primitives[i].oType == 'Point'){
|
|
newFigure.addPrimitive(Point.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Line'){
|
|
newFigure.addPrimitive(Line.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Polyline'){
|
|
newFigure.addPrimitive(Polyline.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Polygon'){
|
|
newFigure.addPrimitive(Polygon.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'DottedPolygon'){
|
|
newFigure.addPrimitive(DottedPolygon.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'QuadCurve'){
|
|
newFigure.addPrimitive(QuadCurve.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'CubicCurve'){
|
|
newFigure.addPrimitive(CubicCurve.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Arc'){
|
|
newFigure.addPrimitive(Arc.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Ellipse'){
|
|
newFigure.addPrimitive(Ellipse.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'DashedArc'){
|
|
newFigure.addPrimitive(DashedArc.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Text'){
|
|
newFigure.addPrimitive(Text.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Path'){
|
|
newFigure.addPrimitive(Path.load(o.primitives[i]))
|
|
}
|
|
else if(o.primitives[i].oType == 'Figure'){
|
|
newFigure.addPrimitive(Figure.load(o.primitives[i])); //kinda recursevly
|
|
}
|
|
else if(o.primitives[i].oType == 'ImageFrame'){
|
|
newFigure.addPrimitive(ImageFrame.load(o.primitives[i])); //kinda recursevly
|
|
}
|
|
}//end for
|
|
|
|
newFigure.groupId = o.groupId;
|
|
newFigure.properties = BuilderProperty.loadArray(o.properties);
|
|
newFigure.style = Style.load(o.style);
|
|
newFigure.rotationCoords = Point.loadArray(o.rotationCoords);
|
|
newFigure.url = o.url;
|
|
|
|
newFigure.CCForm_Shape = o.CCForm_Shape;
|
|
newFigure.CCForm_MyPK = o.CCForm_MyPK;
|
|
|
|
return newFigure ;
|
|
}
|
|
|
|
/**Creates a new {Array} of {Figure}s out of JSON parsed object
|
|
*@param {JSONObject} v - the JSON parsed object
|
|
*@return {Array} of newly constructed {Figure}s
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
*@author Janis Sejans <janis.sejans@towntech.lv>
|
|
**/
|
|
Figure.loadArray = function(v){
|
|
var newFigures = [];
|
|
|
|
for(var i=0; i<v.length; i++){
|
|
newFigures.push(Figure.load(v[i]));
|
|
}
|
|
|
|
return newFigures;
|
|
}
|
|
|
|
Figure.prototype = {
|
|
|
|
constructor: Figure,
|
|
|
|
/* TODO: Remove it!
|
|
* This is wrong as a Figure can have many Text figure inside and picking the first Text
|
|
* is simply wrong
|
|
*
|
|
* Used by the edit panel
|
|
* @return {Text} the text item
|
|
* @deprecated
|
|
*/
|
|
getText: function () {
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
if (this.primitives[i] instanceof Text) {
|
|
return this.primitives[i];
|
|
}
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
/*TODO: Remove it!
|
|
* This is wrong as a Figure can have many Text figure inside set all Text to same
|
|
* text is wrong
|
|
*
|
|
*Set the text from edit panel
|
|
*@param{Text} text - text object
|
|
*@deprecated
|
|
*/
|
|
setText: function (text) {
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
if (this.primitives[i] instanceof Text) {
|
|
this.primitives[i] = text;
|
|
}
|
|
}
|
|
},
|
|
|
|
//@param{bool} transformConnector - should we transform the connector? Used when we transform a figure,
|
|
//without redrawing it.
|
|
transform: function (matrix, transformConnector) {
|
|
if (transformConnector == "undefined" || transformConnector == undefined) {
|
|
transformConnector = true;
|
|
}
|
|
//transform all composing primitives
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
this.primitives[i].transform(matrix);
|
|
}
|
|
|
|
//transform the style
|
|
this.style.transform(matrix);
|
|
|
|
//cascade transform to the connection point
|
|
//Log.info('Figure: transform()');
|
|
if (transformConnector) {
|
|
CONNECTOR_MANAGER.connectionPointTransform(this.id, matrix);
|
|
}
|
|
|
|
//some figures don't have rotation coords, i.e. those that aren't "real" figures, such as the highlight rectangle
|
|
if (this.rotationCoords.length != 0) {
|
|
this.rotationCoords[0].transform(matrix);
|
|
this.rotationCoords[1].transform(matrix);
|
|
}
|
|
},
|
|
|
|
getPoints: function () {
|
|
var points = [];
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
points = points.concat(this.primitives[i].getPoints()); //add all primitive's points in a single pass
|
|
}
|
|
return points;
|
|
},
|
|
|
|
addPrimitive: function (primitive) {
|
|
// add id property to primitive equal its index
|
|
primitive.id = this.primitives.length;
|
|
|
|
this.primitives.push(primitive);
|
|
|
|
// update bound coordinates for gradient
|
|
this.style.gradientBounds = this.getBounds();
|
|
},
|
|
|
|
//no more points to add, so create the handles and selectRect
|
|
finalise: function () {
|
|
var bounds = this.getBounds();
|
|
|
|
if (bounds == null) {
|
|
throw 'Figure bounds are null !!!';
|
|
return;
|
|
}
|
|
//central point of the figure
|
|
this.rotationCoords[0] = new Point(
|
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
|
bounds[1] + (bounds[3] - bounds[1]) / 2
|
|
);
|
|
|
|
//the middle of upper edge
|
|
this.rotationCoords[1] = new Point(this.rotationCoords[0].x, bounds[1]);
|
|
},
|
|
|
|
clone: function () {
|
|
var ret = new Figure(this.name);
|
|
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
ret.addPrimitive(this.primitives[i].clone());
|
|
}
|
|
ret.properties = this.properties.slice(0);
|
|
ret.style = this.style.clone();
|
|
ret.rotationCoords[0] = this.rotationCoords[0].clone();
|
|
ret.rotationCoords[1] = this.rotationCoords[1].clone();
|
|
ret.url = this.url;
|
|
|
|
//get all connection points and add them to the figure
|
|
var cps = CONNECTOR_MANAGER.connectionPointGetAllByParent(this.id);
|
|
|
|
cps.forEach(
|
|
function (connectionPoint) {
|
|
CONNECTOR_MANAGER.connectionPointCreate(ret.id, connectionPoint.point.clone(), ConnectionPoint.TYPE_FIGURE);
|
|
}
|
|
);
|
|
|
|
return ret;
|
|
},
|
|
|
|
/*
|
|
*TODO: this is based on getText which is a WRONG (my or Zack's fault I think)
|
|
*
|
|
*apply/clone another figure style onto this figure
|
|
*@param{Figure} anotherFigure - another figure
|
|
*@author Janis Sejans <janis.sejans@towntech.lv>
|
|
*TODO: From Janis: we don`t have Undo for this operation
|
|
*@deprecated
|
|
*/
|
|
applyAnotherFigureStyle: function (anotherFigure) {
|
|
this.style = anotherFigure.style.clone();
|
|
|
|
var newText = this.getText(); //will contain new text object
|
|
//TODO: From Janis: there is some problem if applying text twice, the getText returns empty string, this means it is not properly cloned
|
|
if (newText instanceof Text) {
|
|
var currTextStr = newText.getTextStr(); //remember text str
|
|
var currTextVector = newText.vector; //remember text vector
|
|
|
|
newText = anotherFigure.getText().clone();
|
|
newText.setTextStr(currTextStr); //restore text str
|
|
newText.vector = currTextVector; //restore text vector
|
|
this.setText(newText);
|
|
}
|
|
},
|
|
|
|
contains: function (x, y) {
|
|
var points = [];
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
if (this.primitives[i].contains(x, y)) {
|
|
return true;
|
|
}
|
|
points = points.concat(this.primitives[i].getPoints());
|
|
}
|
|
return Util.isPointInside(new Point(x, y), points);
|
|
},
|
|
|
|
|
|
/**
|
|
* @return {Array<Number>} - returns [minX, minY, maxX, maxY] - bounds, where
|
|
* all points are in the bounds.
|
|
*/
|
|
getBounds: function () {
|
|
var points = [];
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
var bounds = this.primitives[i].getBounds();
|
|
points.push(new Point(bounds[0], bounds[1]));
|
|
points.push(new Point(bounds[2], bounds[3]));
|
|
}
|
|
return Util.getBounds(points);
|
|
},
|
|
|
|
|
|
paint: function (context) {
|
|
if (this.style) {
|
|
this.style.setupContext(context);
|
|
}
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
context.save();
|
|
var primitive = this.primitives[i];
|
|
|
|
|
|
var oldStyle = null;
|
|
if (primitive.style) { //save primitive's style
|
|
oldStyle = primitive.style.clone();
|
|
}
|
|
|
|
if (primitive.style == null) { //if primitive does not have a style use Figure's one
|
|
primitive.style = this.style.clone();
|
|
}
|
|
else { //if primitive has a style merge it
|
|
primitive.style.merge(this.style);
|
|
}
|
|
|
|
|
|
primitive.paint(context);
|
|
primitive.style = oldStyle;
|
|
|
|
// if(this.style.image != null){ //TODO: should a figure has a Style can't just delegate all to primitives?
|
|
// //clip required for background images, there were two methods, this was the second I tried
|
|
// //neither work in IE
|
|
// context.clip();
|
|
// context.save();
|
|
// if(this.rotationCoords.length != 0){
|
|
// var angle=Util.getAngle(this.rotationCoords[0], this.rotationCoords[1]);
|
|
// if(IE && angle==0){
|
|
// angle=0.00000001;//stupid excanves, without this it puts all images down and right of the correct location
|
|
// //and by an amount relative to the distane from the top left corner
|
|
// }
|
|
//
|
|
// //if we perform a rotation on the actual rotationCoords[0] (centerPoint), when we try to translate it back,
|
|
// //rotationCoords[0] will = 0,0, so we create a clone that does not get changed
|
|
// var rotPoint = this.rotationCoords[0].clone();
|
|
//
|
|
// //move to origin, make a rotation, move back in place
|
|
// this.transform(Matrix.translationMatrix(-rotPoint.x, -rotPoint.y))
|
|
// this.transform(Matrix.rotationMatrix(-angle));
|
|
// this.transform(Matrix.translationMatrix(rotPoint.x, rotPoint.y))
|
|
//
|
|
// //TODO: these are not used...so why the whole acrobatics ?
|
|
// //this was the second method that is also not supported by IE, get the image, place it in
|
|
// //the correct place, then shrink it, so its still an 'image mask' but it is only a small image
|
|
// //context.scale below is also part of this
|
|
// //var shrinkBounds = this.getBounds();
|
|
//
|
|
// //move back to origin, 'undo' the rotation, move back in place
|
|
// this.transform(Matrix.translationMatrix(-rotPoint.x, -rotPoint.y))
|
|
// this.transform(Matrix.rotationMatrix(angle));
|
|
// this.transform(Matrix.translationMatrix(rotPoint.x, rotPoint.y))
|
|
//
|
|
// //rotate current canvas to prepare it to draw the image (you can not roate the image...:D)
|
|
// context.translate(rotPoint.x,rotPoint.y);
|
|
// context.rotate(angle);
|
|
// //context.scale(0.01,0.01)//1/getCanvas().width*shrinkBounds[0]+(shrinkBounds[2]-shrinkBounds[0])/2,1/getCanvas().width*shrinkBounds[1]+(shrinkBounds[3]-shrinkBounds[1])/2)
|
|
// context.translate(-rotPoint.x,-rotPoint.y);
|
|
// }
|
|
// //draw image
|
|
// /*context.fill();
|
|
// context.beginPath();
|
|
// context.globalCompositeOperation = "source-atop"
|
|
// clip works best,but this works too, neither will work in IE*/
|
|
// //context.fill();
|
|
// context.drawImage(this.style.image,this.rotationCoords[0].x-this.style.image.width/2,this.rotationCoords[0].y-this.style.image.height/2,this.style.image.width,this.style.image.height)
|
|
//
|
|
// context.restore();
|
|
// }
|
|
// else if (this.style.image!=null){
|
|
// context.fill();
|
|
// }
|
|
|
|
context.restore();
|
|
}
|
|
},
|
|
|
|
equals: function (anotherFigure) {
|
|
if (!anotherFigure instanceof Figure) {
|
|
Log.info("Figure:equals() 0");
|
|
return false;
|
|
}
|
|
|
|
|
|
if (this.primitives.length == anotherFigure.primitives.length) {
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
if (!this.primitives[i].equals(anotherFigure.primitives[i])) {
|
|
Log.info("Figure:equals() 1");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Log.info("Figure:equals() 2");
|
|
return false;
|
|
}
|
|
//test group
|
|
if (this.groupId != anotherFigure.groupId) {
|
|
return false;
|
|
}
|
|
|
|
//test rotation coords
|
|
if (this.rotationCoords.length == anotherFigure.rotationCoords.length) {
|
|
for (var i in this.rotationCoords) {
|
|
if (!this.rotationCoords[i].equals(anotherFigure.rotationCoords[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
//test style
|
|
if (!this.style.equals(anotherFigure.style)) {
|
|
return false;
|
|
}
|
|
|
|
//test url
|
|
if (!this.url == anotherFigure.url) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
near: function (x, y, radius) {
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
if (this.primitives[i].near(x, y, radius)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
toString: function () {
|
|
var result = this.name + ' [id: ' + this.id + '] (';
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
result += this.primitives[i].toString();
|
|
}
|
|
result += ')';
|
|
return result;
|
|
},
|
|
|
|
|
|
toSVG: function () {
|
|
var tempSVG = '';
|
|
tempSVG += "\n" + repeat("\t", INDENTATION) + "<!--Figure start-->";
|
|
for (var i = 0; i < this.primitives.length; i++) {
|
|
var primitive = this.primitives[i];
|
|
|
|
var oldStyle = null;
|
|
if (primitive.style) { //save primitive's style
|
|
oldStyle = primitive.style.clone();
|
|
}
|
|
|
|
if (primitive.style == null) { //if primitive does not have a style use Figure's one
|
|
primitive.style = this.style;
|
|
}
|
|
else { //if primitive has a style merge it
|
|
primitive.style.merge(this.style);
|
|
}
|
|
|
|
tempSVG += this.primitives[i].toSVG();
|
|
|
|
//URL not exported
|
|
throw Exception("Figure->toSVG->URL not exported");
|
|
|
|
//restore primitives style
|
|
primitive.style = oldStyle;
|
|
}
|
|
tempSVG += "\n" + repeat("\t", INDENTATION) + "<!--Figure end-->" + "\n";
|
|
|
|
return tempSVG;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements a NURBS component in Diagramo
|
|
* @param {Array} points - an {Array} of {Point}s
|
|
* @see http://en.wikipedia.org/wiki/Non-uniform_rational_B-spline
|
|
*
|
|
* http://www.w3.org/Graphics/SVG/IG/resources/svgprimer.html#path_Q
|
|
* http://math.stackexchange.com/questions/92246/aproximate-n-grade-bezier-through-cubic-and-or-quadratic-bezier-curves
|
|
* @see http://stackoverflow.com/questions/1257168/how-do-i-create-a-bezier-curve-to-represent-a-smoothed-polyline
|
|
* @see http://www.codeproject.com/KB/graphics/BezierSpline.aspx Draw a Smooth Curve through a Set of 2D Points with Bezier Primitives
|
|
* "Paul de Casteljau, a brilliant engineer at Citroen"
|
|
* @see http://stackoverflow.com/questions/8369488/splitting-a-bezier-curve
|
|
* @see http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/
|
|
* @see http://devmag.org.za/2011/06/23/bzier-path-algorithms/
|
|
* @see http://drdobbs.com/cpp/184403417 (Forward Difference Calculation of Bezier Curves)
|
|
* @see http://www.timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm
|
|
* @see http://www.algorithmist.net/bezier3.html
|
|
* @see http://www.caffeineowl.com/graphics/2d/vectorial/bezierintro.html
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
function NURBS(points){
|
|
if(points.length < 2){
|
|
throw "NURBS: contructor() We need minimum 3 points to have a NURBS";
|
|
}
|
|
|
|
/**The initial {@link Point}s*/
|
|
this.points = Point.cloneArray(points);
|
|
|
|
/**The array of {CubicCurve} s from which the NURB will be made*/
|
|
this.fragments = this.nurbsPoints(this.points);
|
|
|
|
/**The {@link Style} of the line*/
|
|
this.style = new Style();
|
|
this.style.gradientBounds = this.getBounds();
|
|
|
|
/**Serialization type*/
|
|
this.oType = 'NURBS'; //object type used for JSON deserialization
|
|
}
|
|
|
|
/**Creates a {NURBS} out of JSON parsed object
|
|
*@param {JSONObject} o - the JSON parsed object
|
|
*@return {NURBS} a newly constructed NURBS
|
|
*@author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
NURBS.load = function(o){
|
|
|
|
var newNURBS = new NURBS(Point.loadArray(o.points));
|
|
newNURBS.style = Style.load(o.style);
|
|
return newNURBS;
|
|
};
|
|
|
|
NURBS.prototype = {
|
|
/**Computes a series of Bezier(Cubic) curves to aproximate a curve modeled
|
|
*by a set of points
|
|
*@param {Array}- P and {Array} of {Point}s
|
|
*@return an {Array} of {CubicCurve} (provided also as {Array})
|
|
*Example:
|
|
* [
|
|
* [p1, p2, p3, p4],
|
|
* [p1', p2', p3', p4'],
|
|
* etc
|
|
* ]
|
|
* See /documents/specs/spline-to-bezier.pdf (pages 5 and 6 for a description)
|
|
**/
|
|
nurbsPoints : function (P){
|
|
var n = P.length;
|
|
|
|
/**Contains the gathered sub curves*/
|
|
var sol = [];
|
|
|
|
if(n === 2){
|
|
sol.push(new Line(P[0], P[1]));
|
|
return sol;
|
|
}
|
|
else if(n === 3){
|
|
sol.push(new QuadCurve(P[0], P[1], P[2]));
|
|
return sol;
|
|
}
|
|
else if(n === 4){
|
|
sol.push(new CubicCurve(P[0], P[1], P[2], P[3]));
|
|
return sol;
|
|
}
|
|
|
|
/**Computes factorial
|
|
* @param {Number} k the number
|
|
* */
|
|
function fact(k){
|
|
if(k===0 || k===1){
|
|
return 1;
|
|
}
|
|
else{
|
|
return k * fact(k-1);
|
|
}
|
|
}
|
|
|
|
/**Computes Bernstain*/
|
|
function B(i,n,u){
|
|
return fact(n) / (fact(i) * fact(n-i))* Math.pow(u, i) * Math.pow(1-u, n-i);
|
|
}
|
|
|
|
/**Computes the sum between two points
|
|
*@param p1 - {Point}
|
|
*@param p2 - {Point}
|
|
*@return {Point} the sum of initial points
|
|
**/
|
|
function sum(p1, p2){
|
|
return new Point(p1.x + p2.x, p1.y + p2.y);
|
|
}
|
|
|
|
/**Computes the difference between first {Point} and second {Point}
|
|
*@param p1 - {Point}
|
|
*@param p2 - {Point}
|
|
*@return {Point} the sum of initial points
|
|
**/
|
|
function minus(p1, p2){
|
|
return new Point(p1.x - p2.x, p1.y - p2.y);
|
|
}
|
|
|
|
/**Computes the division of a {Point} by a number
|
|
*@param p - {Point}
|
|
*@param nr - {Number}
|
|
*@return {Point}
|
|
**/
|
|
function divide(p, nr){
|
|
if(nr == 0){
|
|
throw "Division by zero not allowed (yet :) " + this.callee ;
|
|
}
|
|
return new Point(p.x/nr, p.y/nr);
|
|
}
|
|
|
|
/**Computes the multiplication of a {Point} by a number
|
|
*@param p - {Point}
|
|
*@param nr - {Number}
|
|
*@return {Point}
|
|
**/
|
|
function multiply(p, nr){
|
|
return new Point (p.x * nr, p.y * nr);
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
*I do not get why first 4 must be 0 and last 3 of same value.....
|
|
*but otherwise we will get division by zero
|
|
*/
|
|
var k = [0,0,0];
|
|
|
|
var j;
|
|
for(j=0;j<=n-3;j++){
|
|
k.push(j);
|
|
}
|
|
|
|
k.push(n-3, n-3);
|
|
|
|
|
|
|
|
for(i=1; i<=n-3; i++){
|
|
//q1 - compute start point
|
|
var q1 = divide( sum( multiply(P[i], k[i+4] - k[i+2]), multiply(P[i+1], k[i+2] - k[i+1]) ), k[i+4] - k[i+1]);
|
|
|
|
//q0 - compute 1st controll point
|
|
var q_01 = (k[i+3] - k[i+2]) / (k[i+3] - k[i+1]);
|
|
var q_02 = divide( sum( multiply(P[i-1],k[i+3] - k[i+2]), multiply(P[i], k[i+2] - k[i])), k[i+3] - k[i]);
|
|
var q_03 = multiply(q1, ( k[i+2] - k[i+1])/ (k[i+3] - k[i+1]) );
|
|
var q0 = sum(multiply(q_02, q_01), q_03);
|
|
|
|
//q2 - compute 2nd controll point
|
|
var q2 = divide( sum( multiply(P[i], k[i+4] - k[i+3]), multiply(P[i+1], k[i+3] - k[i+1]) ), k[i+4] - k[i+1] );
|
|
|
|
//q3 - compute end point
|
|
var q_31 = (k[i+3] - k[i+2]) / (k[i+4] - k[i+2]);
|
|
var q_32 = divide( sum( multiply(P[i+1], k[i+5] - k[i+3]), multiply(P[i+2], k[i+3] - k[i+2]) ) , k[i+5] - k[i+2]);
|
|
var q_33 = multiply(q2, (k[i+4] - k[i+3])/(k[i+4] - k[i+2]) );
|
|
var q3 = sum(multiply(q_32, q_31), q_33);
|
|
|
|
//store solution
|
|
sol.push( new CubicCurve(q0, q1, q2, q3) );
|
|
}
|
|
|
|
return sol;
|
|
},
|
|
|
|
/**Paint the NURBS*/
|
|
paint : function(context){
|
|
context.beginPath();
|
|
|
|
if(this.style != null){
|
|
this.style.setupContext(context);
|
|
}
|
|
|
|
//Log.info("Nr of cubic curves " + this.fragments.length);
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
fragment.style = this.style.clone();
|
|
fragment.paint(context);
|
|
}
|
|
},
|
|
|
|
|
|
transform : function(matrix){
|
|
//transform initial points
|
|
for(var p = 0; p<this.points.length; p++){
|
|
var point = this.points[p];
|
|
point.transform(matrix);
|
|
}
|
|
|
|
//transform cubic curves
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
fragment.transform(matrix);
|
|
}
|
|
},
|
|
|
|
|
|
/** Tests to see if a point belongs to this NURBS
|
|
* @param {Number} x - the X coordinates
|
|
* @param {Number} y - the Y coordinates
|
|
* @author Alex Gheorghiu <alex@scriptoid.com>
|
|
**/
|
|
contains: function(x, y){
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
if(fragment.contains(x, y)){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**Computes the length of the {NURB} by summing the length of {CubicCurve}s
|
|
* is made of.
|
|
* TODO: as this involves a lot of computations it would nice to use a Math formula
|
|
* */
|
|
getLength : function(){
|
|
var l = 0;
|
|
|
|
for(var ci=0; ci<this.fragments.length; ci++){
|
|
l += this.fragments[ci].getLength();
|
|
}
|
|
return l;
|
|
},
|
|
|
|
/**Get middle point*/
|
|
_deprecated_getMiddle: function(){
|
|
var ci;
|
|
|
|
//gather lengths of curves
|
|
var lengths = [];
|
|
for(ci=0; ci<this.fragments.length; ci++){
|
|
lengths.push(this.fragments[ci].getLength());
|
|
}
|
|
|
|
//find on what curve (index) the middle of NURBE will be
|
|
ci = 0;
|
|
var collectedLength = 0;
|
|
for(ci=0; ci<this.fragments.length; ci++){
|
|
if(collectedLength + this.fragments[ci].getLength() > this.getLength()/2)
|
|
break;
|
|
|
|
collectedLength += this.fragments[ci].getLength();
|
|
}
|
|
|
|
// if (ci == 0 || ci == this.fragments.length)
|
|
// throw "Assert ci it should not be " + ci;
|
|
|
|
var l = this.getLength()/2 - collectedLength;
|
|
var t = l/this.fragments[ci].getLength();
|
|
|
|
return this.fragments[ci].getVisualPoint(t);
|
|
},
|
|
|
|
getMiddle : function() {
|
|
var points = this.getPoints();
|
|
var poly = new Polyline();
|
|
poly.points = points;
|
|
|
|
return poly.getVisualPoint(0.5);
|
|
},
|
|
|
|
equals : function (object){
|
|
throw Exception("Not implemented");
|
|
},
|
|
|
|
toString : function(){
|
|
throw Exception("Not implemented");
|
|
},
|
|
|
|
|
|
toSVG : function() {
|
|
|
|
var result = "\n" + repeat("\t", INDENTATION) + '<path d="';
|
|
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
|
|
result += 'M' + fragment.startPoint.x + ',' + fragment.endPoint.y;
|
|
result += ' C' + fragment.controlPoint1.x + ',' + fragment.controlPoint1.y;
|
|
result += ' ' + fragment.controlPoint2.x + ',' + fragment.controlPoint2.y;
|
|
result += ' ' + fragment.endPoint.x + ',' + fragment.endPoint.y;
|
|
}
|
|
|
|
result += '" style="' + this.style.toSVG() + '" />';
|
|
|
|
return result;
|
|
},
|
|
|
|
clone : function() {
|
|
throw Exception("Not implemented");
|
|
},
|
|
|
|
getBounds: function(){
|
|
return Util.getBounds(this.getPoints());
|
|
},
|
|
|
|
near : function(x, y, radius){
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
if(fragment.near(x, y, radius)){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
getPoints : function(){
|
|
var points = [];
|
|
for(var f=0; f<this.fragments.length; f++){
|
|
var fragment = this.fragments[f];
|
|
points = points.concat(fragment.getPoints());
|
|
}
|
|
return points;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* A predefined matrix of a 90 degree clockwise rotation
|
|
*
|
|
*@see <a href="http://en.wikipedia.org/wiki/Rotation_matrix">http://en.wikipedia.org/wiki/Rotation_matrix</a>
|
|
*/
|
|
var R90 = [
|
|
[Math.cos(0.0872664626),-Math.sin(0.0872664626), 0],
|
|
[Math.sin(0.0872664626), Math.cos(0.0872664626), 0],
|
|
[0, 0, 1]
|
|
];
|
|
|
|
/**
|
|
* A predefined matrix of a 90 degree anti-clockwise rotation
|
|
*
|
|
*@see <a href="http://en.wikipedia.org/wiki/Rotation_matrix">http://en.wikipedia.org/wiki/Rotation_matrix</a>
|
|
*/
|
|
var R90A = [
|
|
[Math.cos(0.0872664626), Math.sin(0.0872664626), 0],
|
|
[-Math.sin(0.0872664626), Math.cos(0.0872664626), 0],
|
|
[0, 0, 1]
|
|
];
|
|
|
|
/**
|
|
* The identity matrix
|
|
*/
|
|
|
|
var IDENTITY=[[1,0,1],[0,1,0],[0,0,1]];
|
|
|
|
|
|
if(typeof(document) == 'undefined'){ //test only from console
|
|
print("\n--==Point==--\n");
|
|
p = new Point(10, 10);
|
|
print(p);
|
|
print("\n");
|
|
p.transform(R90);
|
|
print(p)
|
|
|
|
print("\n--==Line==--\n");
|
|
l = new Line(new Point(10, 23), new Point(34, 50));
|
|
print(l);
|
|
print("\n");
|
|
|
|
|
|
print("\n--==Polyline==--\n");
|
|
polyline = new Polyline();
|
|
for(var i=0;i<5; i++){
|
|
polyline.addPoint(new Point(i, i*i));
|
|
}
|
|
print(polyline);
|
|
print("\n");
|
|
|
|
|
|
|
|
|
|
print("\n--==Quad curve==--\n");
|
|
q = new QuadCurve(new Point(75,25), new Point(25,25), new Point(25,62))
|
|
print(q)
|
|
|
|
print("\n");
|
|
q.transform(R90);
|
|
print(q)
|
|
|
|
print("\n--==Cubic curve==--\n");
|
|
q = new CubicCurve(new Point(75,40), new Point(75,37), new Point(70,25), new Point(50,25))
|
|
print(q)
|
|
|
|
print("\n");
|
|
q.transform(R90);
|
|
print(q)
|
|
|
|
print("\n--==Figure==--\n");
|
|
f = new Figure();
|
|
f.addPrimitive(p);
|
|
f.addPrimitive(q);
|
|
print(f);
|
|
|
|
f.transform(R90);
|
|
print("\n");
|
|
print(f);
|
|
print("\n");
|
|
//f.draw();
|
|
}
|