Source: PlotBoilerplate.js

/**
 * @classdesc The main class of the PlotBoilerplate.
 *
 * @requires Vertex, Line, Vector, Polygon, PBImage, MouseHandler, KeyHandler, VertexAttr, CubicBezierCurve, BezierPath
 *
 * @author   Ikaros Kappler
 * @date     2018-10-23
 * @modified 2018-11-19 Added multi-select and multi-drag.
 * @modified 2018-12-04 Added basic SVG export.
 * @modified 2018-12-09 Extended the constructor (canvas).
 * @modified 2018-12-18 Added the config.redrawOnResize param.
 * @modified 2018-12-18 Added the config.defaultCanvas{Width,Height} params.
 * @modified 2018-12-19 Added CSS scaling.
 * @modified 2018-12-28 Removed the unused 'drawLabel' param. Added the 'enableMouse' and 'enableKeys' params.
 * @modified 2018-12-29 Added the 'drawOrigin' param.
 * @modified 2018-12-29 Renamed the 'autoCenterOffset' param to 'autoAdjustOffset'. Added the params 'offsetAdjustXPercent' and 'offsetAdjustYPercent'.
 * @modified 2019-01-14 Added params 'drawBezierHandleLines' and 'drawBezierHandlePoints'. Added the 'redraw' praam to the add() function.
 * @modified 2019-01-16 Added params 'drawHandleLines' and 'drawHandlePoints'. Added the new params to the dat.gui interface.
 * @modified 2019-01-30 Added the 'Vector' type (extending the Line class).
 * @modified 2019-01-30 Added the 'PBImage' type (a wrapper for images).
 * @modified 2019-02-02 Added the 'canvasWidthFactor' and 'canvasHeightFactor' params.
 * @modified 2019-02-03 Removed the drawBackgroundImage() function, with had no purpose at all. Just add an image to the drawables-list.
 * @modified 2019-02-06 Vertices (instace of Vertex) can now be added. Added the 'draggable' attribute to the vertex attributes.
 * @modified 2019-02-10 Fixed a draggable-bug in PBImage handling (scaling was not possible).
 * @modified 2019-02-10 Added the 'enableTouch' option (default is true).
 * @modified 2019-02-14 Added the console for debugging (setConsole(object)).
 * @modified 2019-02-19 Added two new constants: DEFAULT_CLICK_TOLERANCE and DEFAULT_TOUCH_TOLERANCE.
 * @modified 2019-02-19 Added the second param to the locatePointNear(Vertex,Number) function.
 * @modified 2019-02-20 Removed the 'loadFile' entry from the GUI as it was experimental and never in use.
 * @modified 2019-02-23 Removed the 'rebuild' function as it had no purpose.
 * @modified 2019-02-23 Added scaling of the click-/touch-tolerance with the CSS scale.
 * @modified 2019-03-23 Added JSDoc tags. Changed the default value of config.drawOrigin to false.
 * @modified 2019-04-03 Fixed the touch-drag position detection for canvas elements that are not located at document position (0,0). 
 * @modified 2019-04-03 Tweaked the fit-to-parent function to work with paddings and borders.
 * @version  1.4.2
 *
 * @file PlotBoilerplate
 * @public
 **/


(function(_context) {
    "use strict";

    /** @constant {number} */
    const DEFAULT_CANVAS_WIDTH    = 1024;
    /** @constant {number} */
    const DEFAULT_CANVAS_HEIGHT   =  768;
    /** @constant {number} */
    const DEFAULT_CLICK_TOLERANCE =    8;
    /** @constant {number} */
    const DEFAULT_TOUCH_TOLERANCE =   32;


 
    /** 
     * A helper function to scale elements (usually the canvas) using CSS.
     *
     * transform-origin is at (0,0).
     *
     * @param {HTMLElement} element - The DOM element to scale.
     * @param {number} scaleX The - X scale factor.
     * @param {number} scaleY The - Y scale factor.
     * @return {void}
     **/ 
    var setCSSscale = function( element, scaleX, scaleY ) {
	element.style['transform-origin'] = '0 0';
	if( scaleX==1.0 && scaleY==1.0 ) element.style.transform = null;
	else                             element.style.transform = 'scale(' + scaleX + ',' + scaleY+')';
    };


    /**
     * A wrapper class for draggable items (mostly vertices).
     * @private
     **/
    (function(_context) {
	var Draggable = function( item, type ) {
	    this.item = item;
	    this.type = type;
	    this.vindex = null;
	    this.pindex = null;
	    this.cindex = null;
	};
	Draggable.VERTEX = 'vertex';
	Draggable.prototype.isVertex = function() { return this.type == Draggable.VERTEX; };
	Draggable.prototype.setVIndex = function(vindex) { this.vindex = vindex; return this; };

	_context.Draggable = Draggable;
    })(_context);


    /**
     * Use a special custom attribute set for vertices.
     **/
    VertexAttr.model = { bezierAutoAdjust : false, renderTime : 0, selectable : true, isSelected : false, draggable : true };
    

    /** 
     * The constructor.
     *
     * @constructor
     * @name PlotBoilerplate
     * @param {object} config={} - The configuration.
     * @param {HTMLElement} config.canvas - Your canvas element in the DOM (required).
     * @param {boolean=} [config.fullSize=true] - If set to true the canvas will gain full window size.
     * @param {boolean=} [config.fitToParent=true] - If set to true the canvas will gain the size of its parent container (overrides fullSize).
     * @param {number=} [config.scaleX=1.0] - The initial x-zoom. Default is 1.0.
     * @param {number=} [config.scaleY=1.0] - The initial y-zoom. Default is 1.0.
     * @param {boolean=} [config.rasterGrid=true] - If set to true the background grid will be drawn rastered.
     * @param {number=} [config.rasterAdjustFactor=1.0] - The exponential limit for wrapping down the grid. (2.0 means: halve the grid each 2.0*n zoom step).
     * @param {boolean=} [config.drawOrigin=false] - Draw a crosshair at (0,0).
     * @param {boolean=} [config.autoAdjustOffset=true] -  When set to true then the origin of the XY plane will
     *                         be re-adjusted automatically (see the params
     *                         offsetAdjust{X,Y}Percent for more).
     * @param {number=} [config.offsetAdjustXPercent=50] - The x-fallback position for the origin after
     *                         resizing the canvas.
     * @param {number=} [config.offsetAdjustYPercent=50] - The y-fallback position for the origin after
     *                         resizing the canvas.
     * @param {number=} [config.defaultCanvasWidth=1024] - The canvas size fallback (width) if no automatic resizing
     *                         is switched on. 
     * @param {number=} [config.defaultCanvasHeight=768] - The canvas size fallback (height) if no automatic resizing
     *                         is switched on. 
     * @param {number=} [config.canvasWidthFactor=1.0] - Scaling factor (width) upon the canvas size.
     *                         In combination with cssScale{X,Y} this can be used to obtain
     *                         sub pixel resolutions for retina displays.
     * @param {number=} [config.canvasHeightFactor=1.0] - Scaling factor (height) upon the canvas size.
     *                         In combination with cssScale{X,Y} this can be used to obtain
     *                         sub pixel resolutions for retina displays.
     * @param {number=} [config.cssScaleX=1.0] - Visually resize the canvas (horizontally) using CSS transforms (scale).
     * @param {number=} [config.cssScaleY=1.0] - Visually resize the canvas (vertically) using CSS transforms (scale).
     * @param {boolan=} [config.cssUniformScale=true] - CSS scale x and y obtaining aspect ratio.
     * @param {string=} [config.backgroundColor=#ffffff] - The backround color.
     * @param {boolean=} [config.redrawOnResize=true] - Switch auto-redrawing on resize on/off (some applications
     *                         might want to prevent automatic redrawing to avoid data loss from the draw buffer).
     * @param {boolean=} [config.drawBezierHandleLines=true] - Indicates if Bézier curve handles should be drawn (used for
     *                         editors, no required in pure visualizations).
     * @param {boolean=} [config.drawBezierHandlePoints=true] - Indicates if Bézier curve handle points should be drawn.
     * @param {function=} [config.preDraw=null] - A callback function that will be triggered just before the draw
     *                         function starts.
     * @param {function=} [config.postDraw=null] - A callback function that will be triggered right after the drawing
     *                         process finished.
     * @param {boolean=} [config.enableMouse=true] - Indicates if the application should handle mouse events for you.
     * @param {boolean=} [config.enableTouch=true] - Indicates if the application should handle touch events for you.
     * @param {boolean=} [config.enableTouch=true] - Indicates if the application should handle key events for you.
     */
    var PlotBoilerplate = function( config ) {
	config = config || {};
	if( typeof config.canvas == 'undefined' )
	    throw "No canvas specified.";
	// +---------------------------------------------------------------------------------
	// | A global config that's attached to the dat.gui control interface.
	// +-------------------------------
	this.config = {
	    fullSize              : typeof config.fullSize != 'undefined' ? config.fullSize : true,
	    fitToParent           : typeof config.fitToParent != 'undefined' ? config.fitToParent : true,
	    scaleX                : config.scaleX || 1.0,
	    scaleY                : config.scaleY || 1.0,
	    rasterGrid            : typeof config.rasterGrid != 'undefined' ? config.rasterGrid : true,
	    rasterAdjustFactor    : typeof config.rasterAdjustFactor == 'number' ? config.rasterAdjustFactor : 2.0,
	    drawOrigin            : typeof config.drawOrigin != 'undefined' ? config.drawOrigin : false,
	    autoAdjustOffset      : typeof config.autoAdjustOffset != 'undefined' ? config.autoAdjustOffset : true,
	    offsetAdjustXPercent  : typeof config.offsetAdjustXPercent == 'number' ? config.offsetAdjustXPercent : 50,
	    offsetAdjustYPercent  : typeof config.offsetAdjustYPercent == 'number' ? config.offsetAdjustYPercent : 50,
	    backgroundColor       : config.backgroundColor || '#ffffff',
	    redrawOnResize        : typeof config.redrawOnResize != 'undefined' ? config.redrawOnResize : true,
	    defaultCanvasWidth    : typeof config.defaultCanvasWidth == 'number' ? config.defaultCanvasWidth : DEFAULT_CANVAS_WIDTH,
	    defaultCanvasHeight   : typeof config.defaultCanvasHeight == 'number' ? config.defaultCanvasHeight : DEFAULT_CANVAS_HEIGHT,
	    canvasWidthFactor     : typeof config.canvasWidthFactor == 'number' ? config.canvasWidthFactor : 1.0,
	    canvasHeightFactor    : typeof config.canvasHeightFactor == 'number' ? config.canvasHeightFactor : 1.0,
	    cssScaleX             : typeof config.cssScaleX == 'number' ? config.cssScaleX : 1.0,
	    cssScaleY             : typeof config.cssScaleY == 'number' ? config.cssScaleY : 1.0,
	    cssUniformScale       : typeof config.cssUniformScale != 'undefined' ? config.cssUniformScale : true,
	    rebuild               : function() { rebuild(); },
	    saveFile              : function() { saveFile(); },

	    drawBezierHandleLines : typeof config.drawBezierHandleLines != 'undefined' ? config.drawBezierHandleLines : true,
	    drawBezierHandlePoints : typeof config.drawBezierHandlePoints != 'undefined' ? config.drawBezierHandlePoints : true,
	    drawHandleLines       : typeof config.drawHandleLines != 'undefined' ? config.drawHandleLines : true,
	    drawHandlePoints      : typeof config.drawHandlePoints != 'undefined' ? config.drawHandlePoints : true,
	    
	    // Listeners/observers
	    preDraw               : (typeof config.preDraw == 'function' ? config.preDraw : null),
	    postDraw              : (typeof config.postDraw == 'function' ? config.postDraw : null),

	    // Interaction
	    enableMouse           : typeof config.enableMouse != 'undefined' ? config.enableMouse : true,
	    enableTouch           : typeof config.enableTouch != 'undefined' ? config.enableTouch : true,
	    enableKeys            : typeof config.enableKeys != 'undefined' ? config.enableKeys : true
	};


	// +---------------------------------------------------------------------------------
	// | Object members.
	// +-------------------------------
	this.canvas              = typeof config.canvas == 'string' ? document.getElementById(config.canvas) : config.canvas;
	this.ctx                 = this.canvas.getContext('2d');
	this.draw                = new drawutils(this.ctx,false);
	this.draw.scale.set(this.config.scaleX,this.config.scaleY);
	this.fill                = new drawutils(this.ctx,true);
	this.fill.scale.set(this.config.scaleX,this.config.scaleY);
	this.grid                = new Grid( new Vertex(0,0), new Vertex(50,50) );
	this.image               = null; // An image.
	this.imageBuffer         = null; // A canvas to read the pixel data from.
	this.canvasSize          = { width : DEFAULT_CANVAS_WIDTH, height : DEFAULT_CANVAS_HEIGHT };
	this.vertices            = [];
	this.selectPolygon       = null;
	this.draggedElements     = [];
	this.drawables           = [];
	this.console             = console;
	var _self = this;


	// +---------------------------------------------------------------------------------
	// | After this line: object members.
	// +-------------------------------
	
	/**
	 * Set the console for this instance.
	 *
	 * @method setConsole
	 * @param {object} con - The new console object (default is window.console).
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.setConsole = function( con ) {
	    if( typeof con.log != 'function' ) throw "Console object must have a 'log' function.";
	    if( typeof con.warn != 'function' ) throw "Console object must have a 'warn' function.";
	    if( typeof con.error != 'function' ) throw "Console object must have a 'error' function.";
	    this.console = con;
	};

	
	/**
	 * Update the CSS scale for the canvas depending onf the cssScale{X,Y} settings.<br>
	 * <br>
	 * This function is usually only used inernally.
	 *
	 * @method updateCSSscale
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 * @private
	 **/
	PlotBoilerplate.prototype.updateCSSscale = function() {
	    // this.console.log('update css scale');
	    if( this.config.cssUniformScale ) {
		setCSSscale( this.canvas, this.config.cssScaleX, this.config.cssScaleX );
	    } else {
		setCSSscale( this.canvas, this.config.cssScaleX, this.config.cssScaleY );
	    }
	};
	


	/**
	 * Add a drawable object.<br>
	 * <br>
	 * This must be either:<br>
	 * <pre>
	 *  * a Vertex
	 *  * a Line
	 *  * a Vector
	 *  * a VEllipse
	 *  * a Polygon
	 *  * a BezierPath
	 *  * a BPImage
	 * </pre>
	 *
	 * @param {Object} drawable:Object The drawable (of one of the allowed class instance) to add.
	 * @param {boolean} [redraw=true]
	 * @method add
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.add = function( drawable, redraw ) {
	    if( drawable instanceof Vertex ) {
		this.drawables.push( drawable );
		this.vertices.push( drawable );
	    } else if( drawable instanceof Line ) {
		// Add some lines
		this.drawables.push( drawable );
		this.vertices.push( drawable.a );
		this.vertices.push( drawable.b );
	    } else if( drawable instanceof Vector ) {
		this.drawables.push( drawable );
		this.vertices.push( drawable.a );
		this.vertices.push( drawable.b );
	    } else if( drawable instanceof VEllipse ) {
		this.vertices.push( drawable.center );
		this.vertices.push( drawable.axis );
		this.drawables.push( drawable );
		drawable.center.listeners.addDragListener( function(e) {
		    drawable.axis.add( e.params.dragAmount );
		} ); 
	    } else if( drawable instanceof Polygon ) {
		this.drawables.push( drawable );
		for( var i in drawable.vertices )
		    this.vertices.push( drawable.vertices[i] );
	    } else if( drawable instanceof BezierPath ) {
		this.drawables.push( drawable );
		for( var i in drawable.bezierCurves ) {
		    if( !drawable.adjustCircular && i == 0 )
			this.vertices.push( drawable.bezierCurves[i].startPoint );
		    this.vertices.push( drawable.bezierCurves[i].endPoint );
		    this.vertices.push( drawable.bezierCurves[i].startControlPoint );
		    this.vertices.push( drawable.bezierCurves[i].endControlPoint );
		    drawable.bezierCurves[i].startControlPoint.attr.selectable = false;
		    drawable.bezierCurves[i].endControlPoint.attr.selectable = false;
		}
		for( var i in drawable.bezierCurves ) {	
		    // This should be wrapped into the BezierPath implementation.
		    drawable.bezierCurves[i].startPoint.listeners.addDragListener( function(e) {
			var cindex = drawable.locateCurveByStartPoint( e.params.vertex );
			drawable.bezierCurves[cindex].startPoint.addXY( -e.params.dragAmount.x, -e.params.dragAmount.y );
			drawable.moveCurvePoint( cindex*1, 
						 drawable.START_POINT,         // obtain handle length?
						 e.params.dragAmount           // update arc lengths
					       );
		    } );
		    drawable.bezierCurves[i].startControlPoint.listeners.addDragListener( function(e) {
			var cindex = drawable.locateCurveByStartControlPoint( e.params.vertex );
			if( !drawable.bezierCurves[cindex].startPoint.attr.bezierAutoAdjust )
			    return;
			drawable.adjustPredecessorControlPoint( cindex*1, 
								true,          // obtain handle length?
								true           // update arc lengths
							      );
		    } );
		    drawable.bezierCurves[i].endControlPoint.listeners.addDragListener( function(e) {
			var cindex = drawable.locateCurveByEndControlPoint( e.params.vertex );
			if( !drawable.bezierCurves[(cindex)%drawable.bezierCurves.length].endPoint.attr.bezierAutoAdjust )
			    return;
			drawable.adjustSuccessorControlPoint( cindex*1, 
							      true,            // obtain handle length?
							      true             // update arc lengths
							    );
		    } );
		} // END for
	    } else if( drawable instanceof PBImage ) {
		this.vertices.push( drawable.upperLeft );
		this.vertices.push( drawable.lowerRight );
		this.drawables.push( drawable );
		drawable.upperLeft.listeners.addDragListener( function(e) {
		    drawable.lowerRight.add( e.params.dragAmount );
		} );
		drawable.lowerRight.attr.selectable = false
	    } else {
		throw "Cannot add drawable of unrecognized type: " + drawable.constructor.name;
	    }

	    // This is a workaround for backwards compatibility when the 'redraw' param was not yet present.
	    if( redraw || typeof redraw == 'undefined' )
		this.redraw();
	};


	/**
	 * Draw the grid with the current config settings.<br>
	 *
	 * This function is usually only used internally.
	 *
	 * @method drawGrid
	 * @private
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.drawGrid = function() {
	    var gScale = { x : Grid.utils.mapRasterScale(this.config.rasterAdjustFactor,this.draw.scale.x),
			   y : Grid.utils.mapRasterScale(this.config.rasterAdjustFactor,this.draw.scale.y) };
	    var gSize = { w : this.grid.size.x*gScale.x, h : this.grid.size.y*gScale.y };
	    var cs = { w : this.canvasSize.width/2, h : this.canvasSize.height/2 };
	    var offset = this.draw.offset.clone().inv();
	    offset.x = (Math.round(offset.x+cs.w)/Math.round(gSize.w))*(gSize.w)/this.draw.scale.x + (((this.draw.offset.x-cs.w)/this.draw.scale.x)%gSize.w);
	    offset.y = (Math.round(offset.y+cs.h)/Math.round(gSize.h))*(gSize.h)/this.draw.scale.y + (((this.draw.offset.y-cs.h)/this.draw.scale.x)%gSize.h);
	    if( this.config.rasterGrid )
		this.draw.raster( offset, (this.canvasSize.width)/this.draw.scale.x, (this.canvasSize.height)/this.draw.scale.y, gSize.w, gSize.h, 'rgba(0,128,255,0.125)' );
	    else
		this.draw.grid( offset, (this.canvasSize.width)/this.draw.scale.x, (this.canvasSize.height)/this.draw.scale.y, gSize.w, gSize.h, 'rgba(0,128,255,0.095)' )
	};

	
	/**
	 * Draw the origin with the current config settings.<br>
	 *
	 * This function is usually only used internally.
	 *
	 * @method drawOrigin
	 * @private
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.drawOrigin = function() {
	    // Add a crosshair to mark the origin
	    this.draw.crosshair( { x : 0, y : 0 }, 10, '#000000' );
	};


	/**
	 * Draw all drawables.
	 *
	 * This function is usually only used internally.
	 *
	 * @method drawDrawables
	 * @private
	 * @param {number} renderTime - The current render time. It will be used to distinct 
	 *                              already draw vertices from non-draw-yet vertices.
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.drawDrawables = function( renderTime ) {
	    // Draw drawables
	    for( var i in this.drawables ) {
		var d = this.drawables[i];
		if( d instanceof BezierPath ) {
		    for( var c in d.bezierCurves ) {
			this.draw.cubicBezier( d.bezierCurves[c].startPoint, d.bezierCurves[c].endPoint, d.bezierCurves[c].startControlPoint, d.bezierCurves[c].endControlPoint, '#00a822' );

			if( this.config.drawBezierHandlePoints && this.config.drawHandlePoints ) {
			    if( !d.bezierCurves[c].startPoint.attr.bezierAutoAdjust ) {
				this.draw.diamondHandle( d.bezierCurves[c].startPoint, 7, 'orange' );
				d.bezierCurves[c].startPoint.attr.renderTime = renderTime;
			    }
			    if( !d.bezierCurves[c].endPoint.attr.bezierAutoAdjust ) {
				this.draw.diamondHandle( d.bezierCurves[c].endPoint, 7, 'orange' );
				d.bezierCurves[c].endPoint.attr.renderTime = renderTime;
			    }
			    this.draw.circleHandle( d.bezierCurves[c].startControlPoint, 7, '#008888' );
			    this.draw.circleHandle( d.bezierCurves[c].endControlPoint, 7, '#008888' );
			    d.bezierCurves[c].startControlPoint.attr.renderTime = renderTime;
			    d.bezierCurves[c].endControlPoint.attr.renderTime = renderTime;
			} else {
			    d.bezierCurves[c].startPoint.attr.renderTime = renderTime;
			    d.bezierCurves[c].endPoint.attr.renderTime = renderTime;
			    d.bezierCurves[c].startControlPoint.attr.renderTime = renderTime;
			    d.bezierCurves[c].endControlPoint.attr.renderTime = renderTime;
			}
			
			if( this.config.drawBezierHandleLines && this.config.drawHandleLines ) {
			    this.draw.handleLine( d.bezierCurves[c].startPoint, d.bezierCurves[c].startControlPoint );
			    this.draw.handleLine( d.bezierCurves[c].endPoint, d.bezierCurves[c].endControlPoint );
			}
			
		    }
		} else if( d instanceof Polygon ) {
		    this.draw.polygon( d, '#0022a8' );
		    if( !this.config.drawHandlePoints ) {
			for( var i in d.vertices )
			    d.vertices[i].attr.renderTime = renderTime;
		    }
		} else if( d instanceof VEllipse ) {
		    if( this.config.drawHandleLines ) {
			this.draw.line( d.center.clone().add(0,d.axis.y-d.center.y), d.axis, '#c8c8c8' );
			this.draw.line( d.center.clone().add(d.axis.x-d.center.x,0), d.axis, '#c8c8c8' );
		    }
		    this.draw.ellipse( d.center, Math.abs(d.axis.x-d.center.x), Math.abs(d.axis.y-d.center.y), '#2222a8' );
		    if( !this.config.drawHandlePoints ) {
			d.center.attr.renderTime = renderTime;
			d.axis.attr.renderTime = renderTime;
		    }
		} else if( d instanceof Vertex ) {
		    if( !d.attr.selectable || !d.attr.draggable ) {
			// Draw as special point (grey)
			this.draw.circleHandle( d, 7, '#a8a8a8' );
			d.attr.renderTime = renderTime;
		    }
		} else if( d instanceof Line ) {
		    this.draw.line( d.a, d.b, '#a844a8' );
		    if( !this.config.drawHandlePoints || !d.a.attr.selectable ) 
			d.a.attr.renderTime = renderTime;
		    if( !this.config.drawHandlePoints || !d.b.attr.selectable ) 
			d.b.attr.renderTime = renderTime;
		} else if( d instanceof Vector ) {
		    // this.draw.line( d.a, d.b, '#ff44a8' );
		    this.draw.arrow( d.a, d.b, '#ff44a8' );
		    if( this.config.drawHandlePoints && d.b.attr.selectable ) {
			this.draw.circleHandle( d.b, 7, '#a8a8a8' );
		    } else {
			d.b.attr.renderTime = renderTime;	
		    }
		    // d.a.attr.renderTime = renderTime;
		    if( !this.config.drawHandlePoints || !d.a.attr.selectable ) 
			d.a.attr.renderTime = renderTime;
		    if( !this.config.drawHandlePoints || !d.b.attr.selectable ) 
			d.b.attr.renderTime = renderTime;
		    
		} else if( d instanceof PBImage ) {
		    if( this.config.drawHandleLines )
			this.draw.line( d.upperLeft, d.lowerRight, '#a8a8a8' );
		    this.fill.image( d.image, d.upperLeft, d.lowerRight.clone().sub(d.upperLeft) );
		    if( this.config.drawHandlePoints ) {
			this.draw.circleHandle( d.lowerRight, 7, '#a8a8a8' );
			d.lowerRight.attr.renderTime = renderTime;
		    }
		} else {
		    this.console.error( 'Cannot draw object. Unknown class ' + d.constructor.name + '.' );
		}
	    }
	};



	/**
	 * Draw the select-polygon (if there is one).
	 *
	 * This function is usually only used internally.
	 *
	 * @method drawSelectPolygon
	 * @private
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.drawSelectPolygon = function() {
	    // Draw select polygon?
	    if( this.selectPolygon != null && this.selectPolygon.vertices.length > 0 ) {
		this.draw.polygon( this.selectPolygon, '#888888' );
		this.draw.crosshair( this.selectPolygon.vertices[0], 3, '#008888' );
	    }
	};
	    

	
	/**
	 * Draw all vertices that were not yet drawn with the given render time.<br>
	 * <br>
	 * This function is usually only used internally.
	 *
	 * @method drawVertices
	 * @private
	 * @param {number} renderTime - The current render time. It is used to distinct 
	 *                              already draw vertices from non-draw-yet vertices.
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.drawVertices = function( renderTime ) {
	    // Draw all vertices as small squares if they were not already drawn by other objects
	    for( var i in this.vertices ) {
		if( this.vertices[i].attr.renderTime != renderTime ) {
		    this.draw.squareHandle( this.vertices[i], 5, this.vertices[i].attr.isSelected ? 'rgba(192,128,0)' : 'rgb(0,128,192)' );
		}
	    }
	};
	

	
	/**
	 * Trigger redrawing of all objects.<br>
	 * <br>
	 * Usually this function is automatically called when objects change.
	 *
	 * @method redraw
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.redraw = function() {
	    var renderTime = new Date().getTime();
	    
	    this.clear();
	    if( this.config.preDraw ) this.config.preDraw();
	    
	    this.drawGrid();
	    if( this.config.drawOrigin )
		this.drawOrigin(); 
	    this.drawDrawables(renderTime);
	    this.drawVertices(renderTime);
	    this.drawSelectPolygon();

	    if( this.config.postDraw ) this.config.postDraw();
	    
	}; // END redraw



	/**
	 * This function clears the canvas with the configured background color.<br>
	 * <br>
	 * This function is usually only used internally.
	 *
	 * @method clear
	 * @private
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.clear = function() {
	    // Note that the image might have an alpha channel. Clear the scene first.
	    this.ctx.fillStyle = this.config.backgroundColor; 
	    this.ctx.fillRect(0,0,this.canvasSize.width,this.canvasSize.height);
	};


	/**
	 * Clear the selection.<br>
	 * <br>
	 * This function is usually only used internally.
	 *
	 * @method clearSelection
	 * @private
	 * @param {boolean=} [redraw=false] - Indicates if the redraw function should be triggered.
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {PlotBoilerplate} this
	 **/
	PlotBoilerplate.prototype.clearSelection = function( redraw ) {
	    for( var i in this.vertices ) 
		this.vertices[i].attr.isSelected = false;
	    if( redraw )
		this.redraw();
	    return this;
	};



	/**
	 * This function opens a save-as file dialog and – once an output file is
	 * selected – stores the current canvas contents as an SVG image.
	 *
	 * @method saveFile
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	var saveFile = function() {
	    var svgCode = new SVGBuilder().build( _self.drawables, { canvasSize : _self.canvasSize, offset : _self.draw.offset, zoom : _self.draw.scale } );
	    // See documentation for FileSaver.js for usage.
	    //    https://github.com/eligrey/FileSaver.js
	    var blob = new Blob([svgCode], { type: "image/svg;charset=utf-8" } );
	    saveAs(blob, "plot-boilerplate.svg");
	};


	/**
	 * Get the available inner space of the given container.
	 *
	 * Size minus padding minus border.
	 **/
	// NOT IN USE
	
	var getAvailableContainerSpace = function() {
	    var container = _self.canvas.parentNode;
	    var canvas = _self.canvas;
	    canvas.style.display = 'none';
	    var
	    padding = parseFloat( window.getComputedStyle(container, null).getPropertyValue('padding') ) || 0,
	    border = parseFloat( window.getComputedStyle(canvas, null).getPropertyValue('border-width') ) || 0,
	    pl = parseFloat( window.getComputedStyle(container, null).getPropertyValue('padding-left') ) || padding,
	    pr = parseFloat( window.getComputedStyle(container, null).getPropertyValue('padding-right') ) || padding,
	    pt = parseFloat( window.getComputedStyle(container, null).getPropertyValue('padding-top') ) || padding,
	    pb = parseFloat( window.getComputedStyle(container, null).getPropertyValue('padding-bottom') ) || padding,
	    bl = parseFloat( window.getComputedStyle(canvas, null).getPropertyValue('border-left-width') ) || border,
	    br = parseFloat( window.getComputedStyle(canvas, null).getPropertyValue('border-right-width') ) || border,
	    bt = parseFloat( window.getComputedStyle(canvas, null).getPropertyValue('border-top-width') ) || border,
	    bb = parseFloat( window.getComputedStyle(canvas, null).getPropertyValue('border-bottom-width') ) || border;
	    var w = container.clientWidth; // 1px border
	    var h = container.clientHeight; // 1px border
	    console.log( 'w', w, 'h', h, 'border', border, 'padding', padding, pl, pr, pt, pb, bl, br, bt, bb );
	    canvas.style.display = 'block';
	    return { width : (w-pl-pr-bl-br), height : (h-pt-pb-bt-bb) };
	}; 


	/**
	 * This function resizes the canvas to the required settings (toggles fullscreen).<br>
	 * <br>
	 * This function is usually only used internally but feel free to call it if resizing required.
	 *
	 * @method resizeCanvas
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.resizeCanvas = function() {
	    var _setSize = function(w,h) {
		w *= _self.config.canvasWidthFactor;
		h *= _self.config.canvasHeightFactor;
		_self.canvas.width      = w; 
		_self.canvas.height     = h; 
		_self.canvasSize.width  = w;
		_self.canvasSize.height = h;
		if( _self.config.autoAdjustOffset ) {
		    _self.draw.offset.x = _self.fill.offset.x = w*(_self.config.offsetAdjustXPercent/100); 
		    _self.draw.offset.y = _self.fill.offset.y = h*(_self.config.offsetAdjustYPercent/100);
		}
	    };
	    if( _self.config.fullSize && !_self.config.fitToParent ) {
		// Set editor size
		var width  = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
		var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
		_self.canvas.style.position = 'absolute';
		_self.canvas.style.width = width+'px';
		_self.canvas.style.height = height+'px';
		_self.canvas.style.top = 0;
		_self.canvas.style.left = 0;
		_setSize( width, height );
	    } else if( _self.config.fitToParent ) {
		// Set editor size
		//var width  = _self.canvas.parentNode.clientWidth - 2; // 1px border
		//var height = _self.canvas.parentNode.clientHeight - 2; // 1px border
		//_setSize( width, height );
		_self.canvas.style.position = 'absolute';
		var space = getAvailableContainerSpace( _self.canvas.parentNode );
		_self.canvas.style.width = space.width+'px';
		_self.canvas.style.height = space.height+'px';
		_self.canvas.style.top = null;
		_self.canvas.style.left = null;
		console.log( space );
		_setSize( space.width, space.height );

		//_self.canvas.style.position = 'absolute';
		//_self.canvas.style.width = '100%';
		//_self.canvas.style.height = '100%';
		////_self.canvas.style.top = 0;
		////_self.canvas.style.left = 0;
		//// Now fetch the actual size
		//var width = _self.canvas.clientWidth;
		//var height = _self.canvas.clientHeight;
		//console.log( 'parent.width', width, 'parent.height', height );
		//_setSize(width,height);
		
	    } else {
                _setSize( _self.config.defaultCanvasWidth, _self.config.defaultCanvasHeight );
	    }
	    
	    if( _self.config.redrawOnResize )
		_self.redraw();
	};
	_context.addEventListener( 'resize', this.resizeCanvas );
	this.resizeCanvas();



	/**
	 *  Add all vertices inside the polygon to the current selection.<br>
	 *
	 * @method selectVerticesInPolygon
	 * @param {Polygon} polygon - The polygonal selection area.
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {void}
	 **/
	PlotBoilerplate.prototype.selectVerticesInPolygon = function( polygon ) {
	    for( var i in this.vertices ) {
		if( polygon.containsVert(this.vertices[i]) ) 
		    this.vertices[i].attr.isSelected = true;
	    }
	};
	
	

	/**
	 * (Helper) Locates the point (index) at the passed position. Using an internal tolerance of 7 pixels.
	 *
	 * The result is an object { type : 'bpath', pindex, cindex, pid }
	 *
         * Returns false if no point is near the passed position.  
	 *
	 * @method locatePointNear
	 * @param {Vertex} point - The polygonal selection area.
	 * @param {number=} [tolerance=7] - The tolerance to use identtifying vertices.
	 * @private
	 * @return {Draggable} Or false if none found.
	 **/
        var locatePointNear = function( point, tolerance ) {
            // var tolerance = 7;
	    if( typeof tolerance == 'undefined' )
		tolerance = 7;
	    // Search in vertices
	    for( var vindex in _self.vertices ) {
		var vert = _self.vertices[vindex];
		if( vert.distance(point) < tolerance ) {
		    // { type : 'vertex', vindex : vindex };
		    return new Draggable( vert, Draggable.VERTEX ).setVIndex(vindex); 
		}
	    } 
            return false;
        }


	/**
	 * Handle left-click event.<br> 
	 *
	 * @method handleClick
	 * @param {number} x - The click X position on the canvas.
	 * @param {number} y - The click Y position on the canvas.
	 * @private
	 * @return {void}
	 **/
	function handleClick(x,y) {
	    var p = locatePointNear( _self.transformMousePosition(x, y), DEFAULT_CLICK_TOLERANCE/Math.min(_self.config.cssScaleX,_self.config.cssScaleY) );
	    if( p ) { 
		if( keyHandler.isDown('shift') ) {
		    if( p.type == 'bpath' ) {
			let vert = _self.paths[p.pindex].bezierCurves[p.cindex].getPointByID(p.pid);
			if( vert.attr.selectable )
			    vert.attr.isSelected = !vert.attr.isSelected;
		    } else if( p.type == 'vertex' ) {
			let vert = _self.vertices[p.vindex];
			if( vert.attr.selectable )
			    vert.attr.isSelected = !vert.attr.isSelected;
		    }
		    _self.redraw();
		} else if( keyHandler.isDown('y') /* && p.type=='bpath' && (p.pid==BezierPath.START_POINT || p.pid==BezierPath.END_POINT) */ ) {
		    _self.vertices[p.vindex].attr.bezierAutoAdjust = !_self.vertices[p.vindex].attr.bezierAutoAdjust;
		    _self.redraw();
		}
	    }
	    else if( _self.selectPolygon != null ) {
		var vert = _self.transformMousePosition( x, y );
		_self.selectPolygon.vertices.push( vert );
		_self.redraw();
	    }
	}


	
	/**
	 * Transforms the given x-y-(mouse-)point to coordinates respecting the view offset
	 * and the zoom settings.
	 *
	 * @method transformMousePosition
	 * @param {number} x - The x position relative to the canvas.
	 * @param {number} y - The y position relative to the canvas.
	 * @instance
	 * @memberof PlotBoilerplate
	 * @return {object} A simple object <pre>{ x : Number, y : Number }</pre> with the transformed coordinates.
	 **/
	PlotBoilerplate.prototype.transformMousePosition = function( x, y ) {
	    return { x : (x/this.config.cssScaleX-this.draw.offset.x)/(this.draw.scale.x), y : (y/this.config.cssScaleY-this.draw.offset.y)/(this.draw.scale.y) };
	};
	


	/**
	 * (Helper) The mouse-down handler.
	 *
	 * It selects vertices for dragging.
	 *
	 * @method mouseDownHandler.
	 * @param {Event} e - The event to handle
	 * @private
	 * @return {void}
	 **/
	var mouseDownHandler = function(e) {
	    if( e.which != 1 && !(window.TouchEvent && e.originalEvent instanceof TouchEvent) )
		return; // Only react on left mouse or touch events
	    var p = locatePointNear( _self.transformMousePosition(e.params.pos.x, e.params.pos.y), DEFAULT_CLICK_TOLERANCE/Math.min(_self.config.cssScaleX,_self.config.cssScaleY) );
	    _self.console.log('point at position found: '+p );
	    if( !p ) return;
	    // Drag all selected elements?
	    if( p.type == 'vertex' && _self.vertices[p.vindex].attr.isSelected ) {
		// Multi drag
		for( var i in _self.vertices ) {
		    if( _self.vertices[i].attr.isSelected ) {
			_self.draggedElements.push( new Draggable( _self.vertices[i], Draggable.VERTEX ).setVIndex(i) );
			_self.vertices[i].listeners.fireDragStartEvent( e );
		    }
		}
	    } else {
		// Single drag
		if( !_self.vertices[p.vindex].attr.draggable )
		    return;
		_self.draggedElements.push( p );
		if( p.type == 'bpath' )
		    _self.paths[p.pindex].bezierCurves[p.cindex].getPointByID(p.pid).listeners.fireDragStartEvent( e );
		else if( p.type == 'vertex' )
		    _self.vertices[p.vindex].listeners.fireDragStartEvent( e );
	    }
	    _self.redraw();
	};


	
	/**
	 * The mouse-drag handler.
	 *
	 * It moves selected elements around or performs the panning if the ctrl-key if
	 * hold down.
	 *
	 * @method mouseDownHandler.
	 * @param {Event} e - The event to handle
	 * @private
	 * @return {void}
	 **/
	var mouseDragHandler = function(e) {
	    var oldDragAmount = { x : e.params.dragAmount.x, y : e.params.dragAmount.y };
	    e.params.dragAmount.x /= _self.config.cssScaleX;
	    e.params.dragAmount.y /= _self.config.cssScaleY;
	    if( keyHandler.isDown('alt') || keyHandler.isDown('ctrl') ) {
		_self.draw.offset.add( e.params.dragAmount );
		_self.fill.offset.set( _self.draw.offset );
		_self.redraw();
	    } else {
		// Convert drag amount by scaling
		// Warning: this possibly invalidates the dragEvent for other listeners!
		//          Rethink the solution when other features are added.
		e.params.dragAmount.x /= _self.draw.scale.x;
		e.params.dragAmount.y /= _self.draw.scale.y;
		
		// _self.console.log( 'draggedElements',_self.draggedElements );
		
		for( var i in _self.draggedElements ) {
		    var p = _self.draggedElements[i];
		    // _self.console.log( 'i', i, 'pid', p.pid, 'pindex', p.pindex, 'cindex', p.cindex );
		    if( p.type == 'bpath' ) {
			_self.paths[p.pindex].moveCurvePoint( p.cindex, p.pid, e.params.dragAmount );
			_self.paths[p.pindex].bezierCurves[p.cindex].getPointByID(p.pid).listeners.fireDragEvent( e );
		    } else if( p.type == 'vertex' ) {
			if( !_self.vertices[p.vindex].attr.draggable )
			    continue;
			_self.vertices[p.vindex].add( e.params.dragAmount );
			_self.vertices[p.vindex].listeners.fireDragEvent( e );
		    }
		}
	    }
	    // Restore old event values!
	    e.params.dragAmount.x = oldDragAmount.x;
	    e.params.dragAmount.y = oldDragAmount.y;
	    _self.redraw();
	};

	

	/**
	  * The mouse-up handler.
	 *
	 * It clears the dragging-selection.
	 *
	 * @method mouseDownHandler.
	 * @param {Event} e - The event to handle
	 * @private
	 * @return {void}
	 **/
	var mouseUpHandler = function(e) {
	    if( e.which != 1 )
		return; // Only react on left mouse;
	    if( !e.params.wasDragged )
		handleClick( e.params.pos.x, e.params.pos.y );
	    for( var i in _self.draggedElements ) {
		var p = _self.draggedElements[i];
		if( p.type == 'bpath' ) {
		    _self.paths[p.pindex].bezierCurves[p.cindex].getPointByID(p.pid).listeners.fireDragEndEvent( e );
		} else if( p.type == 'vertex' ) {
		    _self.vertices[p.vindex].listeners.fireDragEndEvent( e );
		}
	    }
	    _self.draggedElements = [];
	    _self.redraw();
	};


	
	/**
	 * The mouse-wheel handler.
	 *
	 * It performs the zooming.
	 *
	 * @method mouseWheelHandler.
	 * @param {Event} e - The event to handle
	 * @private
	 * @return {void}
	 **/
	var mouseWheelHandler = function(e) {
	    var zoomStep = 1.25;
	    if( e.deltaY < 0 ) {
		_self.draw.scale.x = _self.fill.scale.x = _self.config.scaleX = _self.config.scaleX*zoomStep;
		_self.draw.scale.y = _self.fill.scale.y = _self.config.scaleY = _self.config.scaleY*zoomStep;
	    } else if( e.deltaY > 0 ) {
		_self.draw.scale.x = _self.fill.scale.x = _self.config.scaleX = Math.max(_self.config.scaleX/zoomStep,0.01);
		_self.draw.scale.y = _self.fill.scale.y = _self.config.scaleY = Math.max(_self.config.scaleY/zoomStep,0.01);
	    }
	    e.preventDefault();
	    _self.redraw();
	};


	if( this.config.enableMouse ) { 
	    /** +---------------------------------------------------------------------------------
	     * Install a mouse handler on the canvas.
	     **/ // +-------------------------------
	    new MouseHandler(this.canvas)
		.down( mouseDownHandler )
		.drag( mouseDragHandler )
		.up( mouseUpHandler )
		.wheel( mouseWheelHandler )
	    ;
	} else { _self.console.log('Mouse interaction disabled.'); }


	
	if( this.config.enableTouch) { // && typeof TouchHandler != 'undefined' ) { 
	    /** +---------------------------------------------------------------------------------
	     * Install a touch handler on the canvas.
	     **/ // +-------------------------------
	    _self.console.log( 'Installing touch handler' );
	    // +----------------------------------------------------------------------
	    // | Convert absolute touch positions to relative DOM element position (relative to canvas)
	    // +-------------------------------------------------
	    function relPos(pos) {
		// console.log( pos, _self.canvas.offsetLeft, _self.canvas.offsetTop );
		return { x : pos.x - _self.canvas.offsetLeft,
			 y : pos.y - _self.canvas.offsetTop
		       };
	    }
	    // +----------------------------------------------------------------------
	    // | Some private vars to store the current mouse/position/button state.
	    // +-------------------------------------------------
	    var touchMovePos = null;
	    var touchDownPos = null;
	    var draggedElement = null;
	    new Touchy( this.canvas,
			{ one : function( hand, finger ) {
			    touchMovePos = new Vertex( relPos(finger.lastPoint) );
			    touchDownPos = new Vertex( relPos(finger.lastPoint) );
			    draggedElement = locatePointNear( _self.transformMousePosition(touchMovePos.x, touchMovePos.y), DEFAULT_TOUCH_TOLERANCE/Math.min(_self.config.cssScaleX,_self.config.cssScaleY) );
			    if( draggedElement ) {
				hand.on('move', function (points) {
				    //console.log( points );
				    var rel = relPos( points[0] );
				    var trans = _self.transformMousePosition( rel.x, rel.y ); // points[0].x, points[0].y );
				    var diff = new Vertex(_self.transformMousePosition( touchMovePos.x, touchMovePos.y )).difference(trans);
				    if( draggedElement.type == 'vertex' ) {
					if( !_self.vertices[draggedElement.vindex].attr.draggable )
					    return;
					_self.vertices[draggedElement.vindex].add( diff );
					var fakeEvent = { params : { dragAmount : diff.clone(), wasDragged : true, mouseDownPos : touchDownPos.clone(), mouseDragPos : touchDownPos.clone().add(diff) }};
					_self.vertices[draggedElement.vindex].listeners.fireDragEvent( fakeEvent );
					_self.redraw();
				    }
				    touchMovePos = new Vertex(rel); // points[0]);
				} );
			    }

			}
			} );
	} else { _self.console.log('Touch interaction disabled.'); }

	if( this.config.enableKeys ) {
	    // Install key handler
	    var keyHandler = new KeyHandler( { trackAll : true } )
		.down('escape',function() {
		    _self.clearSelection(true);
		} )
		.down('shift',function() {
		_self.selectPolygon = new Polygon();
		    _self.redraw();
		} )
		.up('shift',function() {
		    // Find and select vertices in the drawn area
		    if( _self.selectPolygon == null )
			return;
		    _self.selectVerticesInPolygon( _self.selectPolygon );
		    _self.selectPolygon = null;
		    _self.redraw();
		} )
		.down('e',function() { _self.console.log('e was hit. shift is pressed?',keyHandler.isDown('shift')); } ) 
	    ;
	} // END IF enableKeys?
	else  { _self.console.log('Keyboard interaction disabled.'); }
	
	// Apply the configured CSS scale.
	this.updateCSSscale();	
	// Init	
	this.redraw();
	// Gain focus
	this.canvas.focus();
	
    }; // END construcor 'PlotBoilerplate'



    /**
     * Creates a control GUI (a dat.gui instance) for this 
     * plot boilerplate instance.
     *
     * @method createGUI
     * @instance
     * @memberof PlotBoilerplate
     * @return {dat.gui} 
     **/
    PlotBoilerplate.prototype.createGUI = function() {
	var gui = new dat.gui.GUI();
	var _self = this;
	gui.remember(this.config);
	var fold0 = gui.addFolder('Editor settings');
	var fold00 = fold0.addFolder('Canvas size');
	fold00.add(this.config, 'fullSize').onChange( function() { _self.resizeCanvas(); } ).title("Toggles the fullpage mode.");
	fold00.add(this.config, 'fitToParent').onChange( function() { _self.resizeCanvas(); } ).title("Toggles the fit-to-parent mode to fit to parent container (overrides fullsize).");
	fold00.add(this.config, 'defaultCanvasWidth').min(1).step(10).onChange( function() { _self.resizeCanvas(); } ).title("Specifies the fallback width.");
	fold00.add(this.config, 'defaultCanvasHeight').min(1).step(10).onChange( function() { _self.resizeCanvas(); } ).title("Specifies the fallback height.");
	fold00.add(this.config, 'canvasWidthFactor').min(0.1).step(0.1).max(10).onChange( function() { _self.resizeCanvas(); } ).title("Specifies a factor for the current width.");
	fold00.add(this.config, 'canvasHeightFactor').min(0.1).step(0.1).max(10).onChange( function() { _self.resizeCanvas(); } ).title("Specifies a factor for the current height.");
	fold00.add(this.config, 'cssScaleX').min(0.01).step(0.01).max(1.0).onChange( function() { if(_self.config.cssUniformScale) _self.config.cssScaleY = _self.config.cssScaleX; _self.updateCSSscale(); } ).title("Specifies the visual x scale (CSS).").listen();
	fold00.add(this.config, 'cssScaleY').min(0.01).step(0.01).max(1.0).onChange( function() { if(_self.config.cssUniformScale) _self.config.cssScaleX = _self.config.cssScaleY; _self.updateCSSscale(); } ).title("Specifies the visual y scale (CSS).").listen();
	fold00.add(this.config, 'cssUniformScale').onChange( function() { if(_self.config.cssUniformScale) _self.config.cssScaleY = _self.config.cssScaleX; _self.updateCSSscale(); } ).title("CSS uniform scale (x-scale equlsa y-scale).");
	
	var fold01 = fold0.addFolder('Draw settings');
	fold01.add(this.config, 'drawBezierHandlePoints').onChange( function() { _self.redraw(); } ).title("Draw Bézier handle points.");
	fold01.add(this.config, 'drawBezierHandleLines').onChange( function() { _self.redraw(); } ).title("Draw Bézier handle lines.");
	fold01.add(this.config, 'drawHandlePoints').onChange( function() { _self.redraw(); } ).title("Draw handle points (overrides all other settings).");
	fold01.add(this.config, 'drawHandleLines').onChange( function() { _self.redraw(); } ).title("Draw handle lines in general (overrides all other settings).");
	
	fold0.add(this.config, 'scaleX').title("Scale x.").min(0.01).max(10.0).step(0.01).onChange( function() { _self.draw.scale.x = _self.fill.scale.x = _self.config.scaleX; _self.redraw(); } ).listen();
	fold0.add(this.config, 'scaleY').title("Scale y.").min(0.01).max(10.0).step(0.01).onChange( function() { _self.draw.scale.y = _self.fill.scale.y = _self.config.scaleY; _self.redraw(); } ).listen();
	fold0.add(this.config, 'rasterGrid').title("Draw a fine raster instead a full grid.").onChange( function() { _self.redraw(); } ).listen();
	fold0.add(this.config, 'redrawOnResize').title("Automatically redraw the data if window or canvas is resized.").listen();
	fold0.addColor(this.config, 'backgroundColor').onChange( function() { _self.redraw(); } ).title("Choose a background color.");
	// fold0.add(bp.config, 'loadImage').name('Load Image').title("Load a background image.");
	
	var fold1 = gui.addFolder('Export');
	fold1.add(this.config, 'saveFile').name('Save a file').title("Save as SVG.");	 
	
	return gui;
    };



    /**
     * A set of helper functions.
     * @private
     **/
    PlotBoilerplate.utils = {
	
	/** 
	 * Merge the elements in the 'extension' object into the 'base' object based on
	 * the keys of 'base'.
	 *
	 * @param {Object} base
	 * @param {Object} extension
	 * @return {Object} base extended by the new attributes.
	 **/
	safeMergeByKeys : function( base, extension ) {
	    for( var k in base ) {
		if( !extension.hasOwnProperty(k) )
		    continue;
		var type = typeof base[k];
		try {
		    if( type == 'boolean' ) base[k] = !!JSON.parse(extension[k]);
		    else if( type == 'number' ) base[k] = JSON.parse(extension[k])*1;
		    else if( type == 'function' && typeofextension[k] == 'function' ) base[k] = extension[k] ;
		    else base[k] = extension[k];
		} catch( e ) {
		    _self.console.error( 'error in key ', k, extension[k], e );
		}
	    }
	    return base;
	},

	
	/**
	 * Generate a four-point arrow head, starting at the vector end minus the
	 * arrow head length.
	 *
	 * The first vertex in the returned array is guaranteed to be the located
	 * at the vector line end minus the arrow head length.
	 *
	 *
	 * Due to performance all params are required.
	 *
	 * The params scaleX and scaleY are required for the case that the scaling is not uniform (x and y
	 * scaling different). Arrow heads should not look distored on non-uniform scaling.
	 *
	 * If unsure use 1.0 for scaleX and scaleY (=no distortion).
	 * For headlen use 8, it's a good arrow head size.
	 *
	 * Example:
	 *    buildArrowHead( new Vertex(0,0), new Vertex(50,100), 8, 1.0, 1.0 )
	 *
	 * @param {Vertex} zA - The start vertex of the vector to calculate the arrow head for.
	 * @param {Vertex} zB - The end vertex of the vector.
	 * @param {number} headlen - The length of the arrow head (along the vector direction. A good value is 12).
	 * @param {number} scaleX  - The horizontal scaling during draw.
	 * @param {number} scaleY  - the vertical scaling during draw.
	 **/
	buildArrowHead : function( zA, zB, headlen, scaleX, scaleY ) {
	    var angle = Math.atan2( (zB.y-zA.y)*scaleY, (zB.x-zA.x)*scaleX );
	    
	    var vertices = [];
	    vertices.push( new Vertex(zB.x*scaleX-(headlen)*Math.cos(angle), zB.y*scaleY-(headlen)*Math.sin(angle)) );    
	    vertices.push( new Vertex(zB.x*scaleX-(headlen*1.35)*Math.cos(angle-Math.PI/8), zB.y*scaleY-(headlen*1.35)*Math.sin(angle-Math.PI/8) ) );
	    vertices.push( new Vertex(zB.x*scaleX, zB.y*scaleY) );
	    vertices.push( new Vertex(zB.x*scaleX-(headlen*1.35)*Math.cos(angle+Math.PI/8), zB.y*scaleY-(headlen*1.35)*Math.sin(angle+Math.PI/8)) );

	    return vertices;
	}
    };
    
    _context.PlotBoilerplate = PlotBoilerplate;
    
})(window);