From aa40256e384c13ba197cce9f1e833f3c5a11a8d4 Mon Sep 17 00:00:00 2001 From: Pushkar Joshi Date: Thu, 31 May 2012 18:31:04 -0700 Subject: runtime version of the brush stroke (similar to the pen paths, this seems to not work unless we run it through the debugger) --- assets/canvas-runtime.js | 201 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/assets/canvas-runtime.js b/assets/canvas-runtime.js index 3ed7ed0f..dab1c444 100644 --- a/assets/canvas-runtime.js +++ b/assets/canvas-runtime.js @@ -382,6 +382,12 @@ NinjaCvsRt.GLRuntime = Object.create(Object.prototype, { obj = Object.create(NinjaCvsRt.RuntimeSubPath, {_materials: { value:[], writable:true}}); obj.importJSON (jObj ); break; + + case 6: //brushstroke (created by brush tool) + obj = Object.create(NinjaCvsRt.RuntimeBrushStroke, {_materials: { value:[], writable:true}}); + obj.importJSON (jObj ); + break; + default: throw new Error( "Attempting to load unrecognized object type: " + type ); break; @@ -524,6 +530,7 @@ NinjaCvsRt.RuntimeGeomObj = Object.create(Object.prototype, { GEOM_TYPE_LINE: { value: 3, writable: false }, GEOM_TYPE_PATH: { value: 4, writable: false }, GEOM_TYPE_CUBIC_BEZIER: { value: 5, writable: false }, + GEOM_TYPE_BRUSH_STROKE: { value: 6, writable: false }, GEOM_TYPE_UNDEFINED: { value: -1, writable: false }, /////////////////////////////////////////////////////////////////////// @@ -2006,8 +2013,200 @@ NinjaCvsRt.RuntimeSubPath = Object.create(NinjaCvsRt.RuntimeGeomObj, { ctx.restore(); } } -});// ************************************************************************** +}); + +// ************************************************************************** // END runtime for the pen tool path // ************************************************************************** +// *************************************************************************** +// runtime for brush tool brush stroke +// *************************************************************************** + +NinjaCvsRt.RuntimeBrushStroke = Object.create(NinjaCvsRt.RuntimeGeomObj, { + // array of brush stroke points + _LocalPoints: { value: null, writable: true }, + _OrigLocalPoints: {value: null, writable: true}, + + _strokeWidth: {value: 0, writable: true}, + _strokeColor: {value: null, writable: true}, + _strokeWidth: {value: 0, writable: true}, + _strokeColor: {value: 0, writable: true}, + _strokeHardness: {value: 0, writable: true}, + _strokeUseCalligraphic : {value: 0, writable: true}, + _strokeAngle : {value: 0, writable: true}, + + //stroke smoothing properties + _strokeDoSmoothing: {value: 0, writable: true}, + _strokeAmountSmoothing : {value: 0, writable: true}, + + geomType: { + value: function () { + return this.GEOM_TYPE_BRUSH_STROKE; + } + }, + + _copyCoordinates3D: { + value: function(srcCoord, destCoord){ + var i=0; + var numPoints = srcCoord.length; + for (i=0;i1) { + 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 + + //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._doSmoothing(); //after smoothing, the stroke is ready to be rendered + } + }, + + render: { + value: function() { + // get the world + var world = this.getWorld(); + if (!world) { + throw( "null world in brush stroke render" ); + return; + } + + // get the context + var ctx = world.get2DContext(); + if (!ctx) { + throw( "null world in brush stroke render" ); + return; + } + + ctx.save(); + + //**** BEGIN RENDER CODE BLOCK **** + var points = this._LocalPoints; + var numPoints = points.length; + var tempP, p; + 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; + + //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;t0) { + 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 { + ctx.lineWidth=2; + } + ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; + ctx.translate(disp[0],disp[1]); + ctx.beginPath(); + p = points[0]; + ctx.moveTo(p[0],p[1]); + for (var i=0;i0) { + 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; } +}; - this.setWorld = function (world) { - this._world = world; - }; +BrushStroke.prototype.insertPoint = function(pt, index){ + this._Points.splice(index, 0, pt); + this._isDirty=true; + this._isInit = false; +}; - this.getWorld = function () { - return this._world; - }; +BrushStroke.prototype.isDirty = function(){ + return this._isDirty; +}; - this.geomType = function () { - return this.GEOM_TYPE_BRUSH_STROKE; - }; +BrushStroke.prototype.makeDirty = function(){ + this._isDirty=true; +}; - this.setDrawingTool = function (tool) { - this._drawingTool = tool; - }; +BrushStroke.prototype.getStageWorldCenter = function() { + return this._stageWorldCenter; +}; - this.getDrawingTool = function () { - return this._drawingTool; - }; +BrushStroke.prototype.getBBoxMin = function () { + return this._BBoxMin; +}; - this.setPlaneMatrix = function(planeMat){ - this._planeMat = planeMat; - }; +BrushStroke.prototype.getBBoxMax = function () { + return this._BBoxMax; +}; - this.setPlaneMatrixInverse = function(planeMatInv){ - this._planeMatInv = planeMatInv; - }; +BrushStroke.prototype.getStrokeWidth = function () { + return this._strokeWidth; +}; - this.setPlaneCenter = function(pc){ - this._planeCenter = pc; - }; +BrushStroke.prototype.setStrokeWidth = function (w) { + this._strokeWidth = w; + if (this._strokeWidth<1) { + this._strokeWidth = 1; + } + this._isDirty=true; +}; - this.setDragPlane = function(p){ - this._dragPlane = p; - }; +BrushStroke.prototype.getStrokeMaterial = function () { + return this._strokeMaterial; +}; - this.getNumPoints = function () { - if (this._LocalPoints.length) - return this._LocalPoints.length; - else - return this._Points.length; - }; +BrushStroke.prototype.setStrokeMaterial = function (m) { + this._strokeMaterial = m; this._isDirty = true; +}; - this.getPoint = function (index) { - return this._Points[index].slice(0); - }; +BrushStroke.prototype.getStrokeColor = function () { + return this._strokeColor; +}; - this.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; - } - }; - - this.insertPoint = function(pt, index){ - this._Points.splice(index, 0, pt); - this._isDirty=true; - this._isInit = false; - }; +BrushStroke.prototype.setStrokeColor = function (c) { + this._strokeColor = c; this._isDirty = true; +}; - this.isDirty = function(){ - return this._isDirty; - }; +BrushStroke.prototype.setFillColor = function(c){ + return; +}; //NO-OP for now as we have no fill region - this.makeDirty = function(){ - this._isDirty=true; - }; +BrushStroke.prototype.setSecondStrokeColor = function(c){ + this._secondStrokeColor=c; this._isDirty = true; +}; - this.getStageWorldCenter = function() { - return this._stageWorldCenter; - }; +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; + } +}; - this.getBBoxMin = function () { - return this._BBoxMin; - }; +BrushStroke.prototype.getDoSmoothing = function(){ + return this._strokeDoSmoothing; +}; - this.getBBoxMax = function () { - return this._BBoxMax; - }; +BrushStroke.prototype.setSmoothingAmount = function(a){ + if (this._strokeAmountSmoothing!==a) { + this._strokeAmountSmoothing = a; + this._isDirty = true; + } +}; - this.getStrokeWidth = function () { - return this._strokeWidth; - }; +BrushStroke.prototype.getSmoothingAmount = function(){ + return this._strokeAmountSmoothing; +}; - this.setStrokeWidth = function (w) { - this._strokeWidth = w; - if (this._strokeWidth<1) { - this._strokeWidth = 1; - } - this._isDirty=true; - }; +BrushStroke.prototype.setStrokeUseCalligraphic = function(c){ + if (this._strokeUseCalligraphic!==c){ + this._strokeUseCalligraphic = c; + this._isDirty = true; + } +}; - this.getStrokeMaterial = function () { - return this._strokeMaterial; +BrushStroke.prototype.setStrokeAngle = function(a){ + if (this._strokeAngle!==a){ + this._strokeAngle = a; + this._isDirty = true; }; +}; - this.setStrokeMaterial = function (m) { - this._strokeMaterial = m; this._isDirty = true; - }; +BrushStroke.prototype.getStrokeUseCalligraphic = function(){ + return this._strokeUseCalligraphic; +}; - this.getStrokeColor = function () { - return this._strokeColor; - }; +BrushStroke.prototype.getStrokeAngle = function(){ + return this._strokeAngle; +}; - this.setStrokeColor = function (c) { - this._strokeColor = c; this._isDirty = true; - }; +BrushStroke.prototype.getStrokeStyle = function () { + return this._strokeStyle; +}; - this.setFillColor = function(c){ - return; - }; //NO-OP for now as we have no fill region +BrushStroke.prototype.setStrokeStyle = function (s) { + this._strokeStyle = s; +}; - this.setSecondStrokeColor = function(c){ - this._secondStrokeColor=c; this._isDirty = true; +BrushStroke.prototype.setWidth = function (newW) { + if (newW<1) { + newW=1; //clamp minimum width to 1 } - this.setStrokeHardness = function(h){ - if (this._strokeHardness!==h){ - this._strokeHardness=h; - this._isDirty = true; - } - } - this.getStrokeHardness = function(){ - return this._strokeHardness; + //scale the contents of this subpath to lie within this width + //determine the scale factor by comparing with the old width + var oldWidth = this._BBoxMax[0]-this._BBoxMin[0]; + if (oldWidth<1) { + oldWidth=1; } - this.setDoSmoothing = function(s){ - if (this._strokeDoSmoothing!==s) { - this._strokeDoSmoothing = s; - this._isDirty = true; - } + var scaleX = newW/oldWidth; + if (scaleX===1) { + return; //no need to do anything } - this.getDoSmoothing = function(){ - return this._strokeDoSmoothing; - } + //scale the local point positions such that the width of the bbox is the newW + var origX = this._BBoxMin[0]; + var numPoints = this._LocalPoints.length; + for (var i=0;ithreshold){ + //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 this._MAX_ALLOWED_SAMPLES){ + console.log("leaving the resampling because numPoints is greater than limit:"+this._MAX_ALLOWED_SAMPLES); + break; } - this._isDirty = true; - }; + } + this._Points = newSampledPoints.slice(0); + newSampledPoints = []; +}; - this.getWidth = function() { - if (this._isDirty){ - this.update(); - } - return this._BBoxMax[0]-this._BBoxMin[0]; - }; +BrushStroke.prototype.init = function(){ + if (!this._isInit){ + // **** add samples to the _Points in stageworld space **** + this._addSamples(); - this.getHeight = function() { - if (this._isDirty){ - this.update(); - } - return this._BBoxMax[1]-this._BBoxMin[1]; - }; + // **** compute the 2D (canvas space) coord. of the _Points **** + this._buildLocalCoordFromStageWorldCoord(); - //remove all the points - this.clear = function () { - this._Points = []; - this._OrigLocalPoints = []; - this._isDirty=true; - this._isInit = false; - }; + // **** turn off the init. flag **** + this._isInit = true; + this._isDirty= true; + } - this._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;ithreshold){ - //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 pt[d]) { + bboxMin[d] = pt[d]; } - 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; + if (bboxMax[d] < pt[d]) { + bboxMax[d] = pt[d]; } } - this._Points = newSampledPoints.slice(0); - newSampledPoints = []; - }; + } + //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;i1) { + 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 pt[d]) { - bboxMin[d] = pt[d]; + if (this._BBoxMin[d] > pt[d]) { + this._BBoxMin[d] = pt[d]; } - if (bboxMax[d] < pt[d]) { - bboxMax[d] = pt[d]; + if (this._BBoxMax[d] < pt[d]) { + this._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;i1) { - 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 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 - } - } +//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; - //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); + 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 { - for (var d = 0; d < 3; d++) { - this._BBoxMin[d]-= bboxPadding; - this._BBoxMax[d]+= bboxPadding; - }//for every dimension d from 0 to 2 + gradient = ctx.createLinearGradient(inset, h*0.5, w-inset, h*0.5); } - }; + var colors = ipColor.color; - this.buildBuffers = function () { - //return; //no need to do anything for now - };//buildBuffers() - - //render - // specify how to render the subpath in Canvas2D - this.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) { - 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); + var len = colors.length; + for(n=0; n0) { + 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; } - ctx.lineJoin="bevel"; - ctx.lineCap="butt"; - ctx.globalCompositeOperation = 'source-over'; - ctx.globalAlpha = this._strokeColor[3]; - - for (t=0;t0) { - 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+")"; - //linearly interpolate between the two stroke colors - var currStrokeColor = VecUtils.vecInterpolate(4, this._strokeColor, this._secondStrokeColor, t/numTraces); - //ctx.strokeStyle="rgba("+parseInt(255*currStrokeColor[0])+","+parseInt(255*currStrokeColor[1])+","+parseInt(255*currStrokeColor[2])+","+alphaVal+")"; - ctx.translate(disp[0],disp[1]); - ctx.beginPath(); + } 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.strokeStyle="rgba("+parseInt(255*currStrokeColor[0])+","+parseInt(255*currStrokeColor[1])+","+parseInt(255*currStrokeColor[2])+","+alphaVal+")"; + 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, stageW