diff options
-rw-r--r-- | assets/canvas-runtime.js | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/assets/canvas-runtime.js b/assets/canvas-runtime.js index 4fb0a327..524cdfeb 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 | /////////////////////////////////////////////////////////////////////// |
@@ -2042,8 +2049,197 @@ NinjaCvsRt.RuntimeSubPath = Object.create(NinjaCvsRt.RuntimeGeomObj, { | |||
2042 | } | 2049 | } |
2043 | } | 2050 | } |
2044 | }); | 2051 | }); |
2052 | |||
2045 | // ************************************************************************** | 2053 | // ************************************************************************** |
2046 | // END runtime for the pen tool path | 2054 | // END runtime for the pen tool path |
2047 | // ************************************************************************** | 2055 | // ************************************************************************** |
2048 | 2056 | ||
2049 | 2057 | ||
2058 | // *************************************************************************** | ||
2059 | // runtime for brush tool brush stroke | ||
2060 | // *************************************************************************** | ||
2061 | |||
2062 | NinjaCvsRt.RuntimeBrushStroke = Object.create(NinjaCvsRt.RuntimeGeomObj, { | ||
2063 | // array of brush stroke points | ||
2064 | _LocalPoints: { value: null, writable: true }, | ||
2065 | _OrigLocalPoints: {value: null, writable: true}, | ||
2066 | |||
2067 | _strokeWidth: {value: 0, writable: true}, | ||
2068 | _strokeColor: {value: 0, writable: true}, | ||
2069 | _strokeHardness: {value: 0, writable: true}, | ||
2070 | _strokeUseCalligraphic : {value: 0, writable: true}, | ||
2071 | _strokeAngle : {value: 0, writable: true}, | ||
2072 | |||
2073 | //stroke smoothing properties | ||
2074 | _strokeDoSmoothing: {value: 0, writable: true}, | ||
2075 | _strokeAmountSmoothing : {value: 0, writable: true}, | ||
2076 | |||
2077 | geomType: { | ||
2078 | value: function () { | ||
2079 | return this.GEOM_TYPE_BRUSH_STROKE; | ||
2080 | } | ||
2081 | }, | ||
2082 | |||
2083 | _copyCoordinates3D: { | ||
2084 | value: function(srcCoord, destCoord){ | ||
2085 | var i=0; | ||
2086 | var numPoints = srcCoord.length; | ||
2087 | for (i=0;i<numPoints;i++){ | ||
2088 | destCoord[i] = [srcCoord[i][0],srcCoord[i][1],srcCoord[i][2]]; | ||
2089 | } | ||
2090 | } | ||
2091 | }, | ||
2092 | |||
2093 | _doSmoothing: { | ||
2094 | value: function() { | ||
2095 | var numPoints = this._LocalPoints.length; | ||
2096 | if (this._strokeDoSmoothing && numPoints>1) { | ||
2097 | this._copyCoordinates3D(this._OrigLocalPoints, this._LocalPoints); | ||
2098 | //iterations of Laplacian smoothing (setting the points to the average of their neighbors) | ||
2099 | var numLaplacianIterations = this._strokeAmountSmoothing; | ||
2100 | for (var n=0;n<numLaplacianIterations;n++){ | ||
2101 | var newPoints = this._LocalPoints.slice(0); //I think this performs a copy by reference, which would make the following a SOR step | ||
2102 | for (var i=1;i<numPoints-1;i++) { | ||
2103 | var avgPos = [ 0.5*(this._LocalPoints[i-1][0] + this._LocalPoints[i+1][0]), | ||
2104 | 0.5*(this._LocalPoints[i-1][1] + this._LocalPoints[i+1][1]), | ||
2105 | 0.5*(this._LocalPoints[i-1][2] + this._LocalPoints[i+1][2])] ; | ||
2106 | newPoints[i] = avgPos; | ||
2107 | } | ||
2108 | this._LocalPoints = newPoints.slice(0); | ||
2109 | } | ||
2110 | } | ||
2111 | } | ||
2112 | }, | ||
2113 | |||
2114 | importJSON: { | ||
2115 | value: function(jo) { | ||
2116 | if (this.geomType()!== jo.geomType){ | ||
2117 | return; | ||
2118 | } | ||
2119 | //the geometry for this object | ||
2120 | this._LocalPoints = jo.localPoints.slice(0); | ||
2121 | this._OrigLocalPoints = jo.origLocalPoints.slice(0); | ||
2122 | this._copyCoordinates3D(jo.localPoints, this._LocalPoints); //todo is this necessary in addition to the slice(0) above? | ||
2123 | this._copyCoordinates3D(jo.origLocalPoints, this._OrigLocalPoints); //todo <ditto> | ||
2124 | |||
2125 | //stroke appearance properties | ||
2126 | this._strokeWidth = jo.strokeWidth; | ||
2127 | this._strokeColor = jo.strokeColor; | ||
2128 | this._strokeHardness = jo.strokeHardness; | ||
2129 | this._strokeUseCalligraphic = jo.strokeUseCalligraphic; | ||
2130 | this._strokeAngle = jo.strokeAngle; | ||
2131 | |||
2132 | //stroke smoothing properties | ||
2133 | this._strokeDoSmoothing = jo.strokeDoSmoothing; | ||
2134 | this._strokeAmountSmoothing = jo.strokeAmountSmoothing; | ||
2135 | |||
2136 | this._doSmoothing(); //after smoothing, the stroke is ready to be rendered | ||
2137 | } | ||
2138 | }, | ||
2139 | |||
2140 | render: { | ||
2141 | value: function() { | ||
2142 | // get the world | ||
2143 | var world = this.getWorld(); | ||
2144 | if (!world) { | ||
2145 | throw( "null world in brush stroke render" ); | ||
2146 | return; | ||
2147 | } | ||
2148 | |||
2149 | // get the context | ||
2150 | var ctx = world.get2DContext(); | ||
2151 | if (!ctx) { | ||
2152 | throw( "null world in brush stroke render" ); | ||
2153 | return; | ||
2154 | } | ||
2155 | |||
2156 | ctx.save(); | ||
2157 | |||
2158 | //**** BEGIN RENDER CODE BLOCK **** | ||
2159 | var points = this._LocalPoints; | ||
2160 | var numPoints = points.length; | ||
2161 | var tempP, p; | ||
2162 | if (this._strokeUseCalligraphic) { | ||
2163 | //build the stamp for the brush stroke | ||
2164 | var t=0; | ||
2165 | var numTraces = this._strokeWidth; | ||
2166 | var halfNumTraces = numTraces*0.5; | ||
2167 | var opaqueRegionHalfWidth = 0.5*this._strokeHardness*numTraces*0.01; //the 0.01 is to convert the strokeHardness from [0,100] to [0,1] | ||
2168 | var maxTransparentRegionHalfWidth = halfNumTraces-opaqueRegionHalfWidth; | ||
2169 | |||
2170 | //build an angled (calligraphic) brush stamp | ||
2171 | var deltaDisplacement = [Math.cos(this._strokeAngle),Math.sin(this._strokeAngle)]; | ||
2172 | deltaDisplacement = VecUtils.vecNormalize(2, deltaDisplacement, 1); | ||
2173 | var startPos = [-halfNumTraces*deltaDisplacement[0],-halfNumTraces*deltaDisplacement[1]]; | ||
2174 | |||
2175 | var brushStamp = []; | ||
2176 | for (t=0;t<numTraces;t++){ | ||
2177 | var brushPt = [startPos[0]+t*deltaDisplacement[0], startPos[1]+t*deltaDisplacement[1]]; | ||
2178 | brushStamp.push(brushPt); | ||
2179 | } | ||
2180 | |||
2181 | ctx.lineJoin="bevel"; | ||
2182 | ctx.lineCap="butt"; | ||
2183 | ctx.globalCompositeOperation = 'source-over'; | ||
2184 | ctx.globalAlpha = this._strokeColor[3]; | ||
2185 | |||
2186 | for (t=0;t<numTraces;t++){ | ||
2187 | var disp = [brushStamp[t][0], brushStamp[t][1]]; | ||
2188 | var alphaVal = 1.0; | ||
2189 | var distFromOpaqueRegion = Math.abs(t-halfNumTraces) - opaqueRegionHalfWidth; | ||
2190 | if (distFromOpaqueRegion>0) { | ||
2191 | var transparencyFactor = distFromOpaqueRegion/maxTransparentRegionHalfWidth; | ||
2192 | alphaVal = 1.0 - transparencyFactor;//(transparencyFactor*transparencyFactor);//the square term produces nonlinearly varying alpha values | ||
2193 | alphaVal *= 0.5; //factor that accounts for lineWidth == 2 | ||
2194 | } | ||
2195 | ctx.save(); | ||
2196 | if (t === (numTraces-1) || t === 0){ | ||
2197 | ctx.lineWidth = 1; | ||
2198 | } else { | ||
2199 | ctx.lineWidth=2; | ||
2200 | } | ||
2201 | ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; | ||
2202 | ctx.translate(disp[0],disp[1]); | ||
2203 | ctx.beginPath(); | ||
2204 | p = points[0]; | ||
2205 | ctx.moveTo(p[0],p[1]); | ||
2206 | for (var i=0;i<numPoints;i++){ | ||
2207 | p = points[i]; | ||
2208 | ctx.lineTo(p[0],p[1]); | ||
2209 | } | ||
2210 | ctx.stroke(); | ||
2211 | ctx.restore(); | ||
2212 | } | ||
2213 | } else { | ||
2214 | ctx.globalCompositeOperation = 'lighter'; //we wish to add up the colors | ||
2215 | ctx.globalAlpha = this._strokeColor[3]; | ||
2216 | ctx.lineCap = "round"; | ||
2217 | ctx.lineJoin="round"; | ||
2218 | var minStrokeWidth = (this._strokeHardness*this._strokeWidth)/100; //the hardness is the percentage of the stroke width that's fully opaque | ||
2219 | var numlayers = 1 + Math.ceil((this._strokeWidth-minStrokeWidth)*0.5); | ||
2220 | var alphaVal = 1.0/(numlayers); //this way the alpha at the first path will be 1 | ||
2221 | ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; | ||
2222 | for (var l=0;l<numlayers;l++){ | ||
2223 | ctx.beginPath(); | ||
2224 | p = points[0]; | ||
2225 | ctx.moveTo(p[0],p[1]); | ||
2226 | if (numPoints===1){ | ||
2227 | //display a tiny segment as a single point | ||
2228 | ctx.lineTo(p[0],p[1]+0.01); | ||
2229 | } | ||
2230 | for (var i=1;i<numPoints;i++){ | ||
2231 | p = points[i]; | ||
2232 | ctx.lineTo(p[0],p[1]); | ||
2233 | } | ||
2234 | ctx.lineWidth=2*l+minStrokeWidth; | ||
2235 | ctx.stroke(); | ||
2236 | }//for every layer l | ||
2237 | } //if there is no calligraphic stroke | ||
2238 | |||
2239 | //**** END RENDER CODE BLOCK **** | ||
2240 | |||
2241 | ctx.restore(); | ||
2242 | } | ||
2243 | } | ||
2244 | }); | ||
2245 | |||