/* <copyright> This file contains proprietary software owned by Motorola Mobility, Inc.<br/> No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/> (c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. </copyright> */ var VecUtils = require("js/helper-classes/3D/vec-utils").VecUtils; var GeomObj = require("js/lib/geom/geom-obj").GeomObj; var CanvasController = require("js/controllers/elements/canvas-controller").CanvasController; var ViewUtils = require("js/helper-classes/3D/view-utils").ViewUtils; // Todo: This entire class should be converted to a module /////////////////////////////////////////////////////////////////////// // Class GLBrushStroke // representation a sequence points (polyline) created by brush tool. // Derived from class GLGeomObj /////////////////////////////////////////////////////////////////////// var BrushStroke = function GLBrushStroke() { /////////////////////////////////////////////////// // Instance variables /////////////////////////////////////////////////// this._Points = []; //current state of points in stage-world space (may be different from input) this._LocalPoints = []; //_Points in local coordinates...do this before rendering the points in the canvas this._OrigLocalPoints = []; //copy of input points without any smoothing this._stageWorldCenter = [0,0,0]; //coordinate for the canvas midPoint: a 3D vector in stage world space this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; this._isDirty = true; this._isInit = false; //the HTML5 canvas that holds this brush stroke this._canvas = null; //flag indicating whether or not to freeze the size and position of canvas this._freezeCanvas = false; //stroke information this._strokeWidth = 1.0; this._strokeColor = [0.4, 0.4, 0.4, 1.0]; this._secondStrokeColor = [1, 0.4, 0.4, 1.0]; this._strokeHardness = 100; this._strokeMaterial = null; this._strokeStyle = "Solid"; this._strokeDoSmoothing = false; this._strokeUseCalligraphic = false; this._strokeAngle = 0; this._strokeAmountSmoothing = 0; // currently, brush does not support a fill region this.canFill = true; //threshold that tells us whether two samples are too far apart this._MAX_SAMPLE_DISTANCE_THRESHOLD = 5; //threshold that tells us whether two samples are too close this._MIN_SAMPLE_DISTANCE_THRESHOLD = 2; //prevent extremely long paths that can take a long time to render this._MAX_ALLOWED_SAMPLES = 5000; //drawing context this._world = null; //tool that owns this brushstroke this._drawingTool = null; this._planeMat = null; this._planeMatInv = null; this._planeCenter = null; this._dragPlane = null; }; //BrushStroke class defition BrushStroke.prototype = Object.create(GeomObj, {}); ///////////////////////////////////////////////////////// // Property Accessors/Setters ///////////////////////////////////////////////////////// BrushStroke.prototype.setCanvas = function(c) { this._canvas = c; }; BrushStroke.prototype.setWorld = function (world) { this._world = world; }; BrushStroke.prototype.getWorld = function () { return this._world; }; BrushStroke.prototype.geomType = function () { return this.GEOM_TYPE_BRUSH_STROKE; }; BrushStroke.prototype.setDrawingTool = function (tool) { this._drawingTool = tool; }; BrushStroke.prototype.getDrawingTool = function () { return this._drawingTool; }; BrushStroke.prototype.setPlaneMatrix = function(planeMat){ this._planeMat = planeMat; }; BrushStroke.prototype.setPlaneMatrixInverse = function(planeMatInv){ this._planeMatInv = planeMatInv; }; BrushStroke.prototype.setPlaneCenter = function(pc){ this._planeCenter = pc; }; BrushStroke.prototype.setDragPlane = function(p){ this._dragPlane = p; }; BrushStroke.prototype.getNumPoints = function () { if (this._LocalPoints.length) return this._LocalPoints.length; else return this._Points.length; }; BrushStroke.prototype.getPoint = function (index) { return this._Points[index].slice(0); }; BrushStroke.prototype.addPoint = function (pt) { //add the point only if it is some epsilon away from the previous point var numPoints = this._Points.length; if (numPoints>0) { var threshold = this._MIN_SAMPLE_DISTANCE_THRESHOLD; var prevPt = this._Points[numPoints-1]; var diffPt = [prevPt[0]-pt[0], prevPt[1]-pt[1]]; var diffPtMag = Math.sqrt(diffPt[0]*diffPt[0] + diffPt[1]*diffPt[1]); if (diffPtMag>threshold){ this._Points.push(pt); this._isDirty=true; this._isInit = false; } } else { this._Points.push(pt); this._isDirty=true; this._isInit = false; } }; BrushStroke.prototype.insertPoint = function(pt, index){ this._Points.splice(index, 0, pt); this._isDirty=true; this._isInit = false; }; BrushStroke.prototype.isDirty = function(){ return this._isDirty; }; BrushStroke.prototype.makeDirty = function(){ this._isDirty=true; }; BrushStroke.prototype.getStageWorldCenter = function() { return this._stageWorldCenter; }; BrushStroke.prototype.getBBoxMin = function () { return this._BBoxMin; }; BrushStroke.prototype.getBBoxMax = function () { return this._BBoxMax; }; BrushStroke.prototype.getStrokeWidth = function () { return this._strokeWidth; }; BrushStroke.prototype.setStrokeWidth = function (w) { if (this._strokeWidth!==w) { this._strokeWidth = w; if (this._strokeWidth<1) { this._strokeWidth = 1; } this._isDirty=true; this._freezeCanvas=false; } }; /* BrushStroke.prototype.getStrokeMaterial = function () { return this._strokeMaterial; }; BrushStroke.prototype.setStrokeMaterial = function (m) { this._strokeMaterial = m; this._isDirty = true; }; */ BrushStroke.prototype.getStrokeColor = function () { return null;//return this._strokeColor; }; BrushStroke.prototype.setStrokeColor = function (c) { //this._strokeColor = c; this._isDirty = true; }; BrushStroke.prototype.setFillColor = function(c){ this._strokeColor = c; this._isDirty = true; }; //sets stroke color for now as we have no fill region BrushStroke.prototype.getFillColor = function() { return this._strokeColor; }; //return strokeColor for now as we have no fill region BrushStroke.prototype.setSecondStrokeColor = function(c){ this._secondStrokeColor=c; this._isDirty = true; }; BrushStroke.prototype.setStrokeHardness = function(h){ if (this._strokeHardness!==h){ this._strokeHardness=h; this._isDirty = true; } }; BrushStroke.prototype.getStrokeHardness = function(){ return this._strokeHardness; }; BrushStroke.prototype.setDoSmoothing = function(s){ if (this._strokeDoSmoothing!==s) { this._strokeDoSmoothing = s; this._isDirty = true; } }; BrushStroke.prototype.getDoSmoothing = function(){ return this._strokeDoSmoothing; }; BrushStroke.prototype.setSmoothingAmount = function(a){ if (this._strokeAmountSmoothing!==a) { this._strokeAmountSmoothing = a; this._isDirty = true; this._freezeCanvas=false; } }; BrushStroke.prototype.getSmoothingAmount = function(){ return this._strokeAmountSmoothing; }; BrushStroke.prototype.setStrokeUseCalligraphic = function(c){ if (this._strokeUseCalligraphic!==c){ this._strokeUseCalligraphic = c; this._isDirty = true; } }; BrushStroke.prototype.setStrokeAngle = function(a){ if (this._strokeAngle!==a){ this._strokeAngle = a; this._isDirty = true; }; }; BrushStroke.prototype.getStrokeUseCalligraphic = function(){ return this._strokeUseCalligraphic; }; BrushStroke.prototype.getStrokeAngle = function(){ return this._strokeAngle; }; BrushStroke.prototype.getStrokeStyle = function () { return this._strokeStyle; }; BrushStroke.prototype.setStrokeStyle = function (s) { this._strokeStyle = s; }; BrushStroke.prototype.setWidth = function (newW) { //get the old width from the canvas controller if the canvas is frozen, or from bbox if not frozen. var oldCanvasWidth = parseInt(CanvasController.getProperty(this._canvas, "width")); if (!this._freezeCanvas){ oldCanvasWidth = Math.round(this._BBoxMax[0]-this._BBoxMin[0]); } var minWidth = 1+this._strokeWidth; if (newW<minWidth) { newW=minWidth; } if (oldCanvasWidth<minWidth) { oldCanvasWidth=minWidth; } //scale the contents of this subpath to lie within this width //determine the scale factor by comparing with the old width var scaleX = (newW-this._strokeWidth)/(oldCanvasWidth-this._strokeWidth); if (scaleX===1) { console.log("Ignoring setWidth because scale is "+scaleX); return; //no need to do anything } //scale the local point positions such that the width of the bbox is the newW var origX = 0.5*this._strokeWidth;//this._BBoxMin[0]; //this represents the left edge var numPoints = this._LocalPoints.length; for (var i=0;i<numPoints;i++){ //compute the distance from the left edge var oldW = this._LocalPoints[i][0] - origX; this._LocalPoints[i] = [(origX + oldW*scaleX),this._LocalPoints[i][1],this._LocalPoints[i][2]]; oldW = this._OrigLocalPoints[i][0] - origX; this._OrigLocalPoints[i] = [(origX + oldW*scaleX),this._OrigLocalPoints[i][1],this._OrigLocalPoints[i][2]]; } this._isDirty = true; }; BrushStroke.prototype.setHeight = function (newH) { var oldCanvasHeight = parseInt(CanvasController.getProperty(this._canvas, "height")); if (!this._freezeCanvas){ oldCanvasHeight = this._BBoxMax[1]-this._BBoxMin[1]; } var minHeight = 1 + this._strokeWidth; if (oldCanvasHeight<minHeight) { oldCanvasHeight=minHeight; } if (newH<minHeight) { newH=minHeight; } //scale the contents of this subpath to lie within this height //determine the scale factor by comparing with the old height var scaleY = (newH-this._strokeWidth)/(oldCanvasHeight-this._strokeWidth); if (scaleY===1) { console.log("Ignoring setHeight because scale is 1"); return; //no need to do anything } //scale the local point positions such that the width of the bbox is the newW var origY = 0.5*this._strokeWidth;//this._BBoxMin[1]; //this represents the top edge var numPoints = this._LocalPoints.length; for (var i=0;i<numPoints;i++){ //compute the distance from the bboxMin var oldH = this._LocalPoints[i][1] - origY; this._LocalPoints[i] = [this._LocalPoints[i][0],(origY + oldH*scaleY),this._LocalPoints[i][2]]; oldH = this._OrigLocalPoints[i][1] - origY; this._OrigLocalPoints[i] = [this._OrigLocalPoints[i][0],(origY + oldH*scaleY),this._OrigLocalPoints[i][2]]; } this._isDirty = true; }; BrushStroke.prototype.getWidth = function() { if (this._isDirty){ this.update(); } return this._BBoxMax[0]-this._BBoxMin[0]; }; BrushStroke.prototype.getHeight = function() { if (this._isDirty){ this.update(); } return this._BBoxMax[1]-this._BBoxMin[1]; }; //remove all the points BrushStroke.prototype.clear = function () { this._Points = []; this._OrigLocalPoints = []; this._isDirty=true; this._isInit = false; }; BrushStroke.prototype._addSamples = function() { //**** add samples to the long sections of the path --- Catmull-Rom spline interpolation ***** // instead of the following, may use 4-point subdivision iterations over continuous regions of 'long' segments // look at http://www.gvu.gatech.edu/~jarek/Split&Tweak/ for formula var numPoints = this._Points.length; var numInsertedPoints = 0; var newSampledPoints = []; var threshold = this._MAX_SAMPLE_DISTANCE_THRESHOLD;//this determines whether a segment between two sample too long var prevPt = this._Points[0]; newSampledPoints.push(this._Points[0]); for (var i=1;i<numPoints;i++) { var pt = this._Points[i]; var diff = [pt[0]-prevPt[0], pt[1]-prevPt[1]]; var distance = Math.sqrt(diff[0]*diff[0]+diff[1]*diff[1]); if (distance>threshold){ //build the control polygon for the Catmull-Rom spline (prev. 2 points and next 2 points) var prev = (i===1) ? i-1 : i-2; var next = (i===numPoints-1) ? i : i+1; var ctrlPts = [this._Points[prev], this._Points[i-1], this._Points[i], this._Points[next]]; //insert points along the prev. to current point var numNewPoints = Math.floor(distance/threshold); for (var j=0;j<numNewPoints;j++){ var param = (j+1)/(numNewPoints+1); var newpt = this._CatmullRomSplineInterpolate(ctrlPts, param); newSampledPoints.push(newpt); numInsertedPoints++; } } newSampledPoints.push(pt); prevPt=pt; //end this function if the numPoints has gone above the max. size specified if (numPoints> this._MAX_ALLOWED_SAMPLES){ console.log("leaving the resampling because numPoints is greater than limit:"+this._MAX_ALLOWED_SAMPLES); break; } } this._Points = newSampledPoints.slice(0); newSampledPoints = []; }; BrushStroke.prototype.init = function(){ if (!this._isInit){ // **** add samples to the _Points in stageworld space **** this._addSamples(); // **** compute the 2D (canvas space) coord. of the _Points **** this._buildLocalCoordFromStageWorldCoord(); // **** turn off the init. flag **** this._isInit = true; this._isDirty= true; } // **** update the current brush stroke **** // smoothing, re-compute bounding box, etc. this.update(); }; BrushStroke.prototype._unprojectPt = function(pt, pespectiveDist){ var retPt = pt.slice(0); if (MathUtils.fpCmp(pespectiveDist,-pt[2]) !== 0){ z = pt[2]*pespectiveDist/(pespectiveDist + pt[2]); var x = pt[0]*(pespectiveDist - z)/pespectiveDist, y = pt[1]*(pespectiveDist - z)/pespectiveDist; retPt[0] = x; retPt[1] = y; retPt[2] = z; } return retPt; }; BrushStroke.prototype._buildLocalCoordFromStageWorldCoord = function() { var stage = ViewUtils.getStage(); var stageOffset = ViewUtils.getElementOffset(stage); ViewUtils.setViewportObj(stage); var numPoints = this._Points.length; var i; // ***** compute center of bbox based on stage world coords ***** var bboxMin = [Infinity, Infinity, Infinity]; var bboxMax = [-Infinity, -Infinity, -Infinity]; for (i=0;i<numPoints;i++){ var pt = this._Points[i]; for (var d = 0; d < 3; d++) { if (bboxMin[d] > pt[d]) { bboxMin[d] = pt[d]; } if (bboxMax[d] < pt[d]) { bboxMax[d] = pt[d]; } } } //save the center of the bbox for later use (while constructing the canvas) this._stageWorldCenter = VecUtils.vecInterpolate(3, bboxMin, bboxMax, 0.5); // ***** center the input stageworld data about the center of the bbox ***** this._LocalPoints = []; for (i=0;i<numPoints;i++){ var localPoint = [this._Points[i][0],this._Points[i][1],this._Points[i][2]]; localPoint[0]-= this._stageWorldCenter[0]; localPoint[1]-= this._stageWorldCenter[1]; // ***** unproject all the centered points and convert them to 2D (plane space)***** // (undo the projection step performed by the browser) //localPoint = this._unprojectPt(localPoint, 1400); //todo get the perspective distance from the canvas localPoint = MathUtils.transformPoint(localPoint, this._planeMatInv); //add to the list of local points this._LocalPoints.push(localPoint); } // ***** compute width, height, and midpoint position (in stage world position) of the canvas this._updateBoundingBox(); //compute the bbox to obtain the width and height used below var halfwidth = 0.5*(this._BBoxMax[0]-this._BBoxMin[0]); var halfheight = 0.5*(this._BBoxMax[1]-this._BBoxMin[1]); this._OrigLocalPoints = []; for (i=0;i<numPoints;i++) { this._LocalPoints[i][0]+= halfwidth; this._LocalPoints[i][1]+= halfheight; //store the original points this._OrigLocalPoints.push([this._LocalPoints[i][0],this._LocalPoints[i][1],this._LocalPoints[i][2]]); } //update the bbox with the same adjustment as was made for the local points above this._BBoxMax[0]+= halfwidth;this._BBoxMin[0]+= halfwidth; this._BBoxMax[1]+= halfheight;this._BBoxMin[1]+= halfheight; }; BrushStroke.prototype.update = function() { if (this._isDirty){ // **** do smoothing if necessary **** this._doSmoothing(); // **** recompute the bounding box **** this._updateBoundingBox(); // **** offset the local coords to account for the change in bbox **** this._offsetLocalCoord(-this._BBoxMin[0], -this._BBoxMin[1]); // **** turn off the dirty flag **** this._isDirty = false; } }; BrushStroke.prototype._offsetLocalCoord = function(deltaW, deltaH){ var numPoints = this._LocalPoints.length; for (var i=0;i<numPoints;i++) { this._LocalPoints[i][0]+= deltaW; this._LocalPoints[i][1]+= deltaH; } }; //I had to write this function to do a deep copy because I think slice(0) creates a copy by reference BrushStroke.prototype._copyCoordinates3D = function(srcCoord, destCoord){ var i=0; var numPoints = srcCoord.length; for (i=0;i<numPoints;i++){ destCoord[i] = [srcCoord[i][0],srcCoord[i][1],srcCoord[i][2]]; } }; BrushStroke.prototype._doSmoothing = function() { var numPoints = this._LocalPoints.length; if (this._strokeDoSmoothing && numPoints>1) { this._copyCoordinates3D(this._OrigLocalPoints , this._LocalPoints); //iterations of Laplacian smoothing (setting the points to the average of their neighbors) var numLaplacianIterations = this._strokeAmountSmoothing; for (var n=0;n<numLaplacianIterations;n++){ var newPoints = this._LocalPoints.slice(0); //I think this performs a copy by reference, which would make the following a SOR step for (var i=1;i<numPoints-1;i++) { var avgPos = [ 0.5*(this._LocalPoints[i-1][0] + this._LocalPoints[i+1][0]), 0.5*(this._LocalPoints[i-1][1] + this._LocalPoints[i+1][1]), 0.5*(this._LocalPoints[i-1][2] + this._LocalPoints[i+1][2])] ; newPoints[i] = avgPos; } this._LocalPoints = newPoints.slice(0); } } }; BrushStroke.prototype._updateBoundingBox = function() { // *** compute the bounding box ********* var points = this._LocalPoints; var numPoints = points.length; this._BBoxMin = [Infinity, Infinity, Infinity]; this._BBoxMax = [-Infinity, -Infinity, -Infinity]; if (numPoints === 0) { this._BBoxMin = [0, 0, 0]; this._BBoxMax = [0, 0, 0]; } else { for (var i=0;i<numPoints;i++){ var pt = points[i]; for (var d = 0; d < 3; d++) { if (this._BBoxMin[d] > pt[d]) { this._BBoxMin[d] = pt[d]; } if (this._BBoxMax[d] < pt[d]) { this._BBoxMax[d] = pt[d]; } }//for every dimension d from 0 to 2 } } //increase the bbox given the stroke width and the angle (in case of calligraphic brush) var bboxPadding = this._strokeWidth/2; //todo TEMP! //bboxPadding = 0; //for now, ignore the effect of stroke width on bounding box //end todo TEMP //if (this._strokeUseCalligraphic) { //todo re-enable this if check once we are able to change the left and top of the brush canvas if (false){ this._BBoxMin[0]-= bboxPadding*Math.cos(this._strokeAngle); this._BBoxMin[1]-= bboxPadding*Math.sin(this._strokeAngle); this._BBoxMax[0]+= bboxPadding*Math.cos(this._strokeAngle); this._BBoxMax[1]+= bboxPadding*Math.sin(this._strokeAngle); } else { for (var d = 0; d < 3; d++) { this._BBoxMin[d]-= bboxPadding; this._BBoxMax[d]+= bboxPadding; }//for every dimension d from 0 to 2 } }; BrushStroke.prototype.buildBuffers = function () { //return; //no need to do anything for now };//buildBuffers() //render // specify how to render the subpath in Canvas2D BrushStroke.prototype.render = function () { // get the world var world = this.getWorld(); if (!world){ throw( "null world in brushstroke render" ); } var numPoints = this.getNumPoints(); if (numPoints === 0) { return; //nothing to do for empty paths } if (this._isDirty){ this.update(); } var bboxMin = this.getBBoxMin(); var bboxMax = this.getBBoxMax(); var bboxWidth = bboxMax[0] - bboxMin[0]; var bboxHeight = bboxMax[1] - bboxMin[1]; if (!this._canvas){ //set the canvas by querying the world this._canvas = this.getWorld().getCanvas(); } if (this._canvas && !this._freezeCanvas) { var newLeft = Math.round(this._stageWorldCenter[0] - 0.5 * bboxWidth); var newTop = Math.round(this._stageWorldCenter[1] - 0.5 * bboxHeight); //assign the new position, width, and height as the canvas dimensions through the canvas controller //CanvasController.setProperty(this._canvas, "left", newLeft+"px"); //CanvasController.setProperty(this._canvas, "top", newTop+"px"); CanvasController.setProperty(this._canvas, "width", bboxWidth+"px"); CanvasController.setProperty(this._canvas, "height", bboxHeight+"px"); //this._canvas.elementModel.shapeModel.GLWorld.setViewportFromCanvas(this._canvas); } this._freezeCanvas=true; //unless this is set to false, we will not update the canvas width and height anymore in the render function //get the context var ctx = world.get2DContext(); if (!ctx) { throw ("null context in brushstroke render"); } ctx.save(); ctx.clearRect(0, 0, bboxWidth, bboxHeight); this.drawToContext(ctx, false); ctx.restore(); }; //this.render() //buildColor returns the fillStyle or strokeStyle for the Canvas 2D context BrushStroke.prototype.buildColor = function(ctx, //the 2D rendering context (for creating gradients if necessary) ipColor, //color string, also encodes whether there's a gradient and of what type w, //width of the region of color h, //height of the region of color lw, //linewidth (i.e. stroke width/size) alphaVal) //alpha value for this color (usually computed by the rendering code separately) { if (ipColor.gradientMode){ var position, gradient, cs, inset; //vars used in gradient calculations inset = Math.ceil( lw ) - 0.5; inset = 0; if(ipColor.gradientMode === "radial") { var ww = w - 2*lw, hh = h - 2*lw; gradient = ctx.createRadialGradient(w*0.5, h*0.5, 0, w*0.5, h*0.5, Math.max(ww, hh)*0.5); } else { gradient = ctx.createLinearGradient(inset, h*0.5, w-inset, h*0.5); } var colors = ipColor.color; var len = colors.length; for(n=0; n<len; n++) { position = colors[n].position*0.01; cs = colors[n].value; gradient.addColorStop(position, "rgba(" + cs.r + "," + cs.g + "," + cs.b + "," + alphaVal + ")"); } return gradient; } else { var c = "rgba(" + 255*ipColor[0] + "," + 255*ipColor[1] + "," + 255*ipColor[2] + "," + alphaVal + ")"; return c; } }; BrushStroke.prototype.drawToContext = function(ctx, drawStageWorldPts, stageWorldDeltaX, stageWorldDeltaY, stageWorldToScreenMat){ var points = this._LocalPoints; if (drawStageWorldPts){ //this is usually true when we're drawing the brush stroke on the stage (no canvas yet) points = this._Points; } var numPoints = points.length; var tempP, p, w, h; var world = this.getWorld(); var useBuildColor = false; if (world){ if (this._strokeColor.gradientMode){ useBuildColor = true; } //vars used for the gradient computation in buildColor w = world.getViewportWidth(); h = world.getViewportHeight(); } if (this._strokeUseCalligraphic) { //build the stamp for the brush stroke var t=0; var numTraces = this._strokeWidth; var halfNumTraces = numTraces*0.5; var opaqueRegionHalfWidth = 0.5*this._strokeHardness*numTraces*0.01; //the 0.01 is to convert the strokeHardness from [0,100] to [0,1] var maxTransparentRegionHalfWidth = halfNumTraces-opaqueRegionHalfWidth; //todo this brush stamp should be created outside of this function //build an angled (calligraphic) brush stamp var deltaDisplacement = [Math.cos(this._strokeAngle),Math.sin(this._strokeAngle)]; deltaDisplacement = VecUtils.vecNormalize(2, deltaDisplacement, 1); var startPos = [-halfNumTraces*deltaDisplacement[0],-halfNumTraces*deltaDisplacement[1]]; var brushStamp = []; for (t=0;t<numTraces;t++){ var brushPt = [startPos[0]+t*deltaDisplacement[0], startPos[1]+t*deltaDisplacement[1]]; brushStamp.push(brushPt); } ctx.lineJoin="bevel"; ctx.lineCap="butt"; ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = this._strokeColor[3]; for (t=0;t<numTraces;t++){ var disp = [brushStamp[t][0], brushStamp[t][1]]; var alphaVal = 1.0; var distFromOpaqueRegion = Math.abs(t-halfNumTraces) - opaqueRegionHalfWidth; if (numTraces === 1){ distFromOpaqueRegion = 0; } else if (distFromOpaqueRegion>0) { var transparencyFactor = distFromOpaqueRegion/maxTransparentRegionHalfWidth; alphaVal = 1.0 - transparencyFactor;//(transparencyFactor*transparencyFactor);//the square term produces nonlinearly varying alpha values alphaVal *= 0.5; //factor that accounts for lineWidth == 2 } ctx.save(); if (t === (numTraces-1) || t === 0){ ctx.lineWidth = 1; } else { //todo figure out the correct formula for the line width ctx.lineWidth=2; } if (!useBuildColor){ ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; } else { ctx.strokeStyle = this.buildColor(ctx, this._strokeColor, w, h, this._strokeWidth, alphaVal); } //linearly interpolate between the two stroke colors //var currStrokeColor = VecUtils.vecInterpolate(4, this._strokeColor, this._secondStrokeColor, t/numTraces); ctx.translate(disp[0],disp[1]); ctx.beginPath(); if (drawStageWorldPts) { tempP = points[0].slice(0); tempP[0]+=stageWorldDeltaX; tempP[1]+=stageWorldDeltaY; p = MathUtils.transformAndDivideHomogeneousPoint(tempP, stageWorldToScreenMat); } else { p = points[0]; } ctx.moveTo(p[0],p[1]); for (var i=0;i<numPoints;i++){ if (drawStageWorldPts) { tempP = points[i].slice(0); tempP[0]+=stageWorldDeltaX; tempP[1]+=stageWorldDeltaY; p = MathUtils.transformAndDivideHomogeneousPoint(tempP, stageWorldToScreenMat); } else { p = points[i]; } ctx.lineTo(p[0],p[1]); } ctx.stroke(); ctx.restore(); } } else { ctx.globalCompositeOperation = 'lighter'; //we wish to add up the colors ctx.globalAlpha = this._strokeColor[3]; ctx.lineCap = "round"; ctx.lineJoin="round"; var minStrokeWidth = (this._strokeHardness*this._strokeWidth)/100; //the hardness is the percentage of the stroke width that's fully opaque var numlayers = 1 + Math.ceil((this._strokeWidth-minStrokeWidth)*0.5); var alphaVal = 1.0/(numlayers); //this way the alpha at the first path will be 1 if (!useBuildColor){ ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; } else { ctx.strokeStyle = this.buildColor(ctx, this._strokeColor, w,h, this._strokeWidth, alphaVal); } for (var l=0;l<numlayers;l++){ ctx.beginPath(); if (drawStageWorldPts) { tempP = points[0].slice(0); tempP[0]+=stageWorldDeltaX; tempP[1]+=stageWorldDeltaY; p = MathUtils.transformAndDivideHomogeneousPoint(tempP, stageWorldToScreenMat); } else { p = points[0]; } ctx.moveTo(p[0],p[1]); if (numPoints===1){ //display a tiny segment as a single point ctx.lineTo(p[0],p[1]+0.01); } for (var i=1;i<numPoints;i++){ if (drawStageWorldPts) { tempP = points[i].slice(0); tempP[0]+=stageWorldDeltaX; tempP[1]+=stageWorldDeltaY; p = MathUtils.transformAndDivideHomogeneousPoint(tempP, stageWorldToScreenMat); } else { p = points[i]; } ctx.lineTo(p[0],p[1]); } ctx.lineWidth=2*l+minStrokeWidth; //experiments with shadows /* ctx.shadowOffsetX = 10; ctx.shadowOffsetY = 10; ctx.shadowBlur = 10; ctx.shadowColor = //"rgb("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+")"; "#FF6666"; //or use rgb(red, green, blue) */ ctx.stroke(); }//for every layer l } //if there is no calligraphic stroke }; //this.drawToCanvas() BrushStroke.prototype.exportJSON = function(){ var retObject= new Object(); //the type of this object retObject.type = this.geomType(); retObject.geomType = retObject.type; //the geometry for this object retObject.localPoints = this._LocalPoints.slice(0); this._copyCoordinates3D(this._LocalPoints, retObject.localPoints); //todo is this necessary in addition to the slice(0) above? retObject.origLocalPoints = this._OrigLocalPoints.slice(0); this._copyCoordinates3D(this._OrigLocalPoints, retObject.origLocalPoints); //todo <ditto> retObject.stageWorldCenter = [this._stageWorldCenter[0],this._stageWorldCenter[1],this._stageWorldCenter[2]]; retObject.planeMat = this._planeMat; retObject.planeMatInv = this._planeMatInv; retObject.dragPlane = [this._dragPlane[0],this._dragPlane[1],this._dragPlane[2],this._dragPlane[3]]; //stroke appearance properties retObject.strokeWidth = this._strokeWidth; retObject.strokeColor = this._strokeColor; retObject.strokeHardness = this._strokeHardness; retObject.strokeUseCalligraphic = this._strokeUseCalligraphic; retObject.strokeAngle = this._strokeAngle; //stroke smoothing properties retObject.strokeDoSmoothing = this._strokeDoSmoothing; retObject.strokeAmountSmoothing = this._strokeAmountSmoothing; return retObject; }; BrushStroke.prototype.importJSON = function(jo){ if (this.geomType()!== jo.geomType){ return; } //the geometry for this object this._LocalPoints = jo.localPoints.slice(0); this._copyCoordinates3D(jo.localPoints, this._LocalPoints); //todo is this necessary in addition to the slice(0) above? this._OrigLocalPoints = jo.origLocalPoints.slice(0); this._copyCoordinates3D(jo.origLocalPoints, this._OrigLocalPoints); //todo <ditto> this._stageWorldCenter = [jo.stageWorldCenter[0],jo.stageWorldCenter[1],jo.stageWorldCenter[2]]; this._planeMat = jo.planeMat; this._planeMatInv = jo.planeMatInv; this._dragPlane = [jo.dragPlane[0],jo.dragPlane[1],jo.dragPlane[2],jo.dragPlane[3]]; //stroke appearance properties this._strokeWidth = jo.strokeWidth; this._strokeColor = jo.strokeColor; this._strokeHardness = jo.strokeHardness; this._strokeUseCalligraphic = jo.strokeUseCalligraphic; this._strokeAngle = jo.strokeAngle; //stroke smoothing properties this._strokeDoSmoothing = jo.strokeDoSmoothing; this._strokeAmountSmoothing = jo.strokeAmountSmoothing; this._isInit = true; //do not re-initialize this brush stroke this._isDirty = true; //force a re-computation of meta-geometry before rendering this.update(); //after this, the stroke is ready to be rendered }; BrushStroke.prototype.collidesWithPoint = function (x, y, z) { if (x < this._BBoxMin[0]) return false; if (x > this._BBoxMax[0]) return false; if (y < this._BBoxMin[1]) return false; if (y > this._BBoxMax[1]) return false; if (z < this._BBoxMin[2]) return false; if (z > this._BBoxMax[2]) return false; return true; }; BrushStroke.prototype.collidesWithPoint = function (x, y) { if (x < this._BBoxMin[0]) return false; if (x > this._BBoxMax[0]) return false; if (y < this._BBoxMin[1]) return false; if (y > this._BBoxMax[1]) return false; return true; }; BrushStroke.prototype._CatmullRomSplineInterpolate = function(ctrlPts, t) { //perform CatmullRom interpolation on the spline...assume t is in [0,1] var t2 = t*t; var t3 = t2*t; var retPoint = [0,0,0]; for (var i=0;i<3;i++){ retPoint[i] = 0.5 *( (2*ctrlPts[1][i]) + (-ctrlPts[0][i] + ctrlPts[2][i]) * t + (2*ctrlPts[0][i] - 5*ctrlPts[1][i] + 4*ctrlPts[2][i] - ctrlPts[3][i]) * t2 + (-ctrlPts[0][i] + 3*ctrlPts[1][i]- 3*ctrlPts[2][i] + ctrlPts[3][i]) * t3); } return retPoint; } if (typeof exports === "object") { exports.BrushStroke = BrushStroke; }