diff options
-rw-r--r-- | assets/canvas-runtime.js | 199 |
1 files changed, 198 insertions, 1 deletions
diff --git a/assets/canvas-runtime.js b/assets/canvas-runtime.js index 3ed7ed0f..b5b08cea 100644 --- a/assets/canvas-runtime.js +++ b/assets/canvas-runtime.js | |||
@@ -382,6 +382,12 @@ NinjaCvsRt.GLRuntime = Object.create(Object.prototype, { | |||
382 | obj = Object.create(NinjaCvsRt.RuntimeSubPath, {_materials: { value:[], writable:true}}); | 382 | obj = Object.create(NinjaCvsRt.RuntimeSubPath, {_materials: { value:[], writable:true}}); |
383 | obj.importJSON (jObj ); | 383 | obj.importJSON (jObj ); |
384 | break; | 384 | break; |
385 | |||
386 | case 6: //brushstroke (created by brush tool) | ||
387 | obj = Object.create(NinjaCvsRt.RuntimeBrushStroke, {_materials: { value:[], writable:true}}); | ||
388 | obj.importJSON (jObj ); | ||
389 | break; | ||
390 | |||
385 | default: | 391 | default: |
386 | throw new Error( "Attempting to load unrecognized object type: " + type ); | 392 | throw new Error( "Attempting to load unrecognized object type: " + type ); |
387 | break; | 393 | break; |
@@ -524,6 +530,7 @@ NinjaCvsRt.RuntimeGeomObj = Object.create(Object.prototype, { | |||
524 | GEOM_TYPE_LINE: { value: 3, writable: false }, | 530 | GEOM_TYPE_LINE: { value: 3, writable: false }, |
525 | GEOM_TYPE_PATH: { value: 4, writable: false }, | 531 | GEOM_TYPE_PATH: { value: 4, writable: false }, |
526 | GEOM_TYPE_CUBIC_BEZIER: { value: 5, writable: false }, | 532 | GEOM_TYPE_CUBIC_BEZIER: { value: 5, writable: false }, |
533 | GEOM_TYPE_BRUSH_STROKE: { value: 6, writable: false }, | ||
527 | GEOM_TYPE_UNDEFINED: { value: -1, writable: false }, | 534 | GEOM_TYPE_UNDEFINED: { value: -1, writable: false }, |
528 | 535 | ||
529 | /////////////////////////////////////////////////////////////////////// | 536 | /////////////////////////////////////////////////////////////////////// |
@@ -2006,8 +2013,198 @@ NinjaCvsRt.RuntimeSubPath = Object.create(NinjaCvsRt.RuntimeGeomObj, { | |||
2006 | ctx.restore(); | 2013 | ctx.restore(); |
2007 | } | 2014 | } |
2008 | } | 2015 | } |
2009 | });// ************************************************************************** | 2016 | }); |
2017 | |||
2018 | // ************************************************************************** | ||
2010 | // END runtime for the pen tool path | 2019 | // END runtime for the pen tool path |
2011 | // ************************************************************************** | 2020 | // ************************************************************************** |
2012 | 2021 | ||
2013 | 2022 | ||
2023 | // *************************************************************************** | ||
2024 | // runtime for brush tool brush stroke | ||
2025 | // *************************************************************************** | ||
2026 | |||
2027 | NinjaCvsRt.RuntimeBrushStroke = Object.create(NinjaCvsRt.RuntimeGeomObj, { | ||
2028 | // array of brush stroke points | ||
2029 | _LocalPoints: { value: null, writable: true }, | ||
2030 | _OrigLocalPoints: {value: null, writable: true}, | ||
2031 | |||
2032 | _strokeWidth: {value: 0, writable: true}, | ||
2033 | _strokeColor: {value: 0, writable: true}, | ||
2034 | _strokeHardness: {value: 0, writable: true}, | ||
2035 | _strokeUseCalligraphic : {value: 0, writable: true}, | ||
2036 | _strokeAngle : {value: 0, writable: true}, | ||
2037 | |||
2038 | //stroke smoothing properties | ||
2039 | _strokeDoSmoothing: {value: 0, writable: true}, | ||
2040 | _strokeAmountSmoothing : {value: 0, writable: true}, | ||
2041 | |||
2042 | geomType: { | ||
2043 | value: function () { | ||
2044 | return this.GEOM_TYPE_BRUSH_STROKE; | ||
2045 | } | ||
2046 | }, | ||
2047 | |||
2048 | _copyCoordinates3D: { | ||
2049 | value: function(srcCoord, destCoord){ | ||
2050 | var i=0; | ||
2051 | var numPoints = srcCoord.length; | ||
2052 | for (i=0;i<numPoints;i++){ | ||
2053 | destCoord[i] = [srcCoord[i][0],srcCoord[i][1],srcCoord[i][2]]; | ||
2054 | } | ||
2055 | } | ||
2056 | }, | ||
2057 | |||
2058 | _doSmoothing: { | ||
2059 | value: function() { | ||
2060 | var numPoints = this._LocalPoints.length; | ||
2061 | if (this._strokeDoSmoothing && numPoints>1) { | ||
2062 | this._copyCoordinates3D(this._OrigLocalPoints, this._LocalPoints); | ||
2063 | //iterations of Laplacian smoothing (setting the points to the average of their neighbors) | ||
2064 | var numLaplacianIterations = this._strokeAmountSmoothing; | ||
2065 | for (var n=0;n<numLaplacianIterations;n++){ | ||
2066 | var newPoints = this._LocalPoints.slice(0); //I think this performs a copy by reference, which would make the following a SOR step | ||
2067 | for (var i=1;i<numPoints-1;i++) { | ||
2068 | var avgPos = [ 0.5*(this._LocalPoints[i-1][0] + this._LocalPoints[i+1][0]), | ||
2069 | 0.5*(this._LocalPoints[i-1][1] + this._LocalPoints[i+1][1]), | ||
2070 | 0.5*(this._LocalPoints[i-1][2] + this._LocalPoints[i+1][2])] ; | ||
2071 | newPoints[i] = avgPos; | ||
2072 | } | ||
2073 | this._LocalPoints = newPoints.slice(0); | ||
2074 | } | ||
2075 | } | ||
2076 | } | ||
2077 | }, | ||
2078 | |||
2079 | importJSON: { | ||
2080 | value: function(jo) { | ||
2081 | if (this.geomType()!== jo.geomType){ | ||
2082 | return; | ||
2083 | } | ||
2084 | //the geometry for this object | ||
2085 | this._LocalPoints = jo.localPoints.slice(0); | ||
2086 | this._OrigLocalPoints = jo.origLocalPoints.slice(0); | ||
2087 | this._copyCoordinates3D(jo.localPoints, this._LocalPoints); //todo is this necessary in addition to the slice(0) above? | ||
2088 | this._copyCoordinates3D(jo.origLocalPoints, this._OrigLocalPoints); //todo <ditto> | ||
2089 | |||
2090 | //stroke appearance properties | ||
2091 | this._strokeWidth = jo.strokeWidth; | ||
2092 | this._strokeColor = jo.strokeColor; | ||
2093 | this._strokeHardness = jo.strokeHardness; | ||
2094 | this._strokeUseCalligraphic = jo.strokeUseCalligraphic; | ||
2095 | this._strokeAngle = jo.strokeAngle; | ||
2096 | |||
2097 | //stroke smoothing properties | ||
2098 | this._strokeDoSmoothing = jo.strokeDoSmoothing; | ||
2099 | this._strokeAmountSmoothing = jo.strokeAmountSmoothing; | ||
2100 | |||
2101 | this._doSmoothing(); //after smoothing, the stroke is ready to be rendered | ||
2102 | } | ||
2103 | }, | ||
2104 | |||
2105 | render: { | ||
2106 | value: function() { | ||
2107 | // get the world | ||
2108 | var world = this.getWorld(); | ||
2109 | if (!world) { | ||
2110 | throw( "null world in brush stroke render" ); | ||
2111 | return; | ||
2112 | } | ||
2113 | |||
2114 | // get the context | ||
2115 | var ctx = world.get2DContext(); | ||
2116 | if (!ctx) { | ||
2117 | throw( "null world in brush stroke render" ); | ||
2118 | return; | ||
2119 | } | ||
2120 | |||
2121 | ctx.save(); | ||
2122 | |||
2123 | //**** BEGIN RENDER CODE BLOCK **** | ||
2124 | var points = this._LocalPoints; | ||
2125 | var numPoints = points.length; | ||
2126 | var tempP, p; | ||
2127 | if (this._strokeUseCalligraphic) { | ||
2128 | //build the stamp for the brush stroke | ||
2129 | var t=0; | ||
2130 | var numTraces = this._strokeWidth; | ||
2131 | var halfNumTraces = numTraces*0.5; | ||
2132 | var opaqueRegionHalfWidth = 0.5*this._strokeHardness*numTraces*0.01; //the 0.01 is to convert the strokeHardness from [0,100] to [0,1] | ||
2133 | var maxTransparentRegionHalfWidth = halfNumTraces-opaqueRegionHalfWidth; | ||
2134 | |||
2135 | //build an angled (calligraphic) brush stamp | ||
2136 | var deltaDisplacement = [Math.cos(this._strokeAngle),Math.sin(this._strokeAngle)]; | ||
2137 | deltaDisplacement = VecUtils.vecNormalize(2, deltaDisplacement, 1); | ||
2138 | var startPos = [-halfNumTraces*deltaDisplacement[0],-halfNumTraces*deltaDisplacement[1]]; | ||
2139 | |||
2140 | var brushStamp = []; | ||
2141 | for (t=0;t<numTraces;t++){ | ||
2142 | var brushPt = [startPos[0]+t*deltaDisplacement[0], startPos[1]+t*deltaDisplacement[1]]; | ||
2143 | brushStamp.push(brushPt); | ||
2144 | } | ||
2145 | |||
2146 | ctx.lineJoin="bevel"; | ||
2147 | ctx.lineCap="butt"; | ||
2148 | ctx.globalCompositeOperation = 'source-over'; | ||
2149 | ctx.globalAlpha = this._strokeColor[3]; | ||
2150 | |||
2151 | for (t=0;t<numTraces;t++){ | ||
2152 | var disp = [brushStamp[t][0], brushStamp[t][1]]; | ||
2153 | var alphaVal = 1.0; | ||
2154 | var distFromOpaqueRegion = Math.abs(t-halfNumTraces) - opaqueRegionHalfWidth; | ||
2155 | if (distFromOpaqueRegion>0) { | ||
2156 | var transparencyFactor = distFromOpaqueRegion/maxTransparentRegionHalfWidth; | ||
2157 | alphaVal = 1.0 - transparencyFactor;//(transparencyFactor*transparencyFactor);//the square term produces nonlinearly varying alpha values | ||
2158 | alphaVal *= 0.5; //factor that accounts for lineWidth == 2 | ||
2159 | } | ||
2160 | ctx.save(); | ||
2161 | if (t === (numTraces-1) || t === 0){ | ||
2162 | ctx.lineWidth = 1; | ||
2163 | } else { | ||
2164 | ctx.lineWidth=2; | ||
2165 | } | ||
2166 | ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; | ||
2167 | ctx.translate(disp[0],disp[1]); | ||
2168 | ctx.beginPath(); | ||
2169 | p = points[0]; | ||
2170 | ctx.moveTo(p[0],p[1]); | ||
2171 | for (var i=0;i<numPoints;i++){ | ||
2172 | p = points[i]; | ||
2173 | ctx.lineTo(p[0],p[1]); | ||
2174 | } | ||
2175 | ctx.stroke(); | ||
2176 | ctx.restore(); | ||
2177 | } | ||
2178 | } else { | ||
2179 | ctx.globalCompositeOperation = 'lighter'; //we wish to add up the colors | ||
2180 | ctx.globalAlpha = this._strokeColor[3]; | ||
2181 | ctx.lineCap = "round"; | ||
2182 | ctx.lineJoin="round"; | ||
2183 | var minStrokeWidth = (this._strokeHardness*this._strokeWidth)/100; //the hardness is the percentage of the stroke width that's fully opaque | ||
2184 | var numlayers = 1 + Math.ceil((this._strokeWidth-minStrokeWidth)*0.5); | ||
2185 | var alphaVal = 1.0/(numlayers); //this way the alpha at the first path will be 1 | ||
2186 | ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; | ||
2187 | for (var l=0;l<numlayers;l++){ | ||
2188 | ctx.beginPath(); | ||
2189 | p = points[0]; | ||
2190 | ctx.moveTo(p[0],p[1]); | ||
2191 | if (numPoints===1){ | ||
2192 | //display a tiny segment as a single point | ||
2193 | ctx.lineTo(p[0],p[1]+0.01); | ||
2194 | } | ||
2195 | for (var i=1;i<numPoints;i++){ | ||
2196 | p = points[i]; | ||
2197 | ctx.lineTo(p[0],p[1]); | ||
2198 | } | ||
2199 | ctx.lineWidth=2*l+minStrokeWidth; | ||
2200 | ctx.stroke(); | ||
2201 | }//for every layer l | ||
2202 | } //if there is no calligraphic stroke | ||
2203 | |||
2204 | //**** END RENDER CODE BLOCK **** | ||
2205 | |||
2206 | ctx.restore(); | ||
2207 | } | ||
2208 | } | ||
2209 | }); | ||
2210 | |||