diff options
-rw-r--r-- | assets/canvas-runtime.js | 250 | ||||
-rwxr-xr-x | js/data/pi/pi-data.js | 5 | ||||
-rwxr-xr-x | js/lib/geom/brush-stroke.js | 1423 | ||||
-rw-r--r-- | js/tools/BrushTool.js | 2 |
4 files changed, 997 insertions, 683 deletions
diff --git a/assets/canvas-runtime.js b/assets/canvas-runtime.js index 4fb0a327..76332bcb 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,251 @@ 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 | //buildColor returns the fillStyle or strokeStyle for the Canvas 2D context | ||
2141 | buildColor: { | ||
2142 | value: function(ctx, //the 2D rendering context (for creating gradients if necessary) | ||
2143 | ipColor, //color string, also encodes whether there's a gradient and of what type | ||
2144 | w, //width of the region of color | ||
2145 | h, //height of the region of color | ||
2146 | lw) //linewidth (i.e. stroke width/size) | ||
2147 | { | ||
2148 | if (ipColor.gradientMode){ | ||
2149 | var position, gradient, cs, inset; //vars used in gradient calculations | ||
2150 | inset = Math.ceil( lw ) - 0.5; | ||
2151 | |||
2152 | if(ipColor.gradientMode === "radial") { | ||
2153 | var ww = w - 2*lw, hh = h - 2*lw; | ||
2154 | gradient = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, Math.max(ww, hh)/2); | ||
2155 | } else { | ||
2156 | gradient = ctx.createLinearGradient(inset, h/2, w-inset, h/2); | ||
2157 | } | ||
2158 | var colors = ipColor.color; | ||
2159 | |||
2160 | var len = colors.length; | ||
2161 | for(n=0; n<len; n++) { | ||
2162 | position = colors[n].position/100; | ||
2163 | cs = colors[n].value; | ||
2164 | gradient.addColorStop(position, "rgba(" + cs.r + "," + cs.g + "," + cs.b + "," + cs.a + ")"); | ||
2165 | } | ||
2166 | return gradient; | ||
2167 | } else { | ||
2168 | var c = "rgba(" + 255*ipColor[0] + "," + 255*ipColor[1] + "," + 255*ipColor[2] + "," + ipColor[3] + ")"; | ||
2169 | return c; | ||
2170 | } | ||
2171 | } | ||
2172 | }, | ||
2173 | |||
2174 | render: { | ||
2175 | value: function() { | ||
2176 | //vars for gradient code | ||
2177 | var w,h; | ||
2178 | |||
2179 | // get the world | ||
2180 | var world = this.getWorld(); | ||
2181 | if (!world) { | ||
2182 | throw( "null world in brush stroke render" ); | ||
2183 | return; | ||
2184 | } else { | ||
2185 | |||
2186 | if (this._strokeColor.gradientMode){ | ||
2187 | useBuildColor = true; | ||
2188 | } | ||
2189 | //vars used for the gradient computation in buildColor | ||
2190 | w = world.getViewportWidth(); | ||
2191 | h = world.getViewportHeight(); | ||
2192 | } | ||
2193 | // get the context | ||
2194 | var ctx = world.get2DContext(); | ||
2195 | if (!ctx) { | ||
2196 | throw( "null world in brush stroke render" ); | ||
2197 | return; | ||
2198 | } | ||
2199 | |||
2200 | ctx.save(); | ||
2201 | |||
2202 | //**** BEGIN RENDER CODE BLOCK **** | ||
2203 | var points = this._LocalPoints; | ||
2204 | var numPoints = points.length; | ||
2205 | var tempP, p; | ||
2206 | if (this._strokeUseCalligraphic) { | ||
2207 | //build the stamp for the brush stroke | ||
2208 | var t=0; | ||
2209 | var numTraces = this._strokeWidth; | ||
2210 | var halfNumTraces = numTraces*0.5; | ||
2211 | var opaqueRegionHalfWidth = 0.5*this._strokeHardness*numTraces*0.01; //the 0.01 is to convert the strokeHardness from [0,100] to [0,1] | ||
2212 | var maxTransparentRegionHalfWidth = halfNumTraces-opaqueRegionHalfWidth; | ||
2213 | |||
2214 | //build an angled (calligraphic) brush stamp | ||
2215 | var deltaDisplacement = [Math.cos(this._strokeAngle),Math.sin(this._strokeAngle)]; | ||
2216 | deltaDisplacement = this.vecNormalize(2, deltaDisplacement, 1); | ||
2217 | var startPos = [-halfNumTraces*deltaDisplacement[0],-halfNumTraces*deltaDisplacement[1]]; | ||
2218 | |||
2219 | var brushStamp = []; | ||
2220 | for (t=0;t<numTraces;t++){ | ||
2221 | var brushPt = [startPos[0]+t*deltaDisplacement[0], startPos[1]+t*deltaDisplacement[1]]; | ||
2222 | brushStamp.push(brushPt); | ||
2223 | } | ||
2224 | |||
2225 | ctx.lineJoin="bevel"; | ||
2226 | ctx.lineCap="butt"; | ||
2227 | ctx.globalCompositeOperation = 'source-over'; | ||
2228 | ctx.globalAlpha = this._strokeColor[3]; | ||
2229 | |||
2230 | for (t=0;t<numTraces;t++){ | ||
2231 | var disp = [brushStamp[t][0], brushStamp[t][1]]; | ||
2232 | var alphaVal = 1.0; | ||
2233 | var distFromOpaqueRegion = Math.abs(t-halfNumTraces) - opaqueRegionHalfWidth; | ||
2234 | if (distFromOpaqueRegion>0) { | ||
2235 | var transparencyFactor = distFromOpaqueRegion/maxTransparentRegionHalfWidth; | ||
2236 | alphaVal = 1.0 - transparencyFactor;//(transparencyFactor*transparencyFactor);//the square term produces nonlinearly varying alpha values | ||
2237 | alphaVal *= 0.5; //factor that accounts for lineWidth == 2 | ||
2238 | } | ||
2239 | ctx.save(); | ||
2240 | if (t === (numTraces-1) || t === 0){ | ||
2241 | ctx.lineWidth = 1; | ||
2242 | } else { | ||
2243 | ctx.lineWidth=2; | ||
2244 | } | ||
2245 | if (!useBuildColor){ | ||
2246 | ctx.strokeStyle="rgba("+parseInt(255*this._strokeColor[0])+","+parseInt(255*this._strokeColor[1])+","+parseInt(255*this._strokeColor[2])+","+alphaVal+")"; | ||
2247 | } else { | ||