1 /****************************************************************************
  2  Copyright (c) 2008-2010 Ricardo Quesada
  3  Copyright (c) 2011-2012 cocos2d-x.org
  4  Copyright (c) 2013-2014 Chukong Technologies Inc.
  5  Copyright (c) 2012 Pierre-David BĂ©langer
  6 
  7  http://www.cocos2d-x.org
  8 
  9  Permission is hereby granted, free of charge, to any person obtaining a copy
 10  of this software and associated documentation files (the "Software"), to deal
 11  in the Software without restriction, including without limitation the rights
 12  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 13  copies of the Software, and to permit persons to whom the Software is
 14  furnished to do so, subject to the following conditions:
 15 
 16  The above copyright notice and this permission notice shall be included in
 17  all copies or substantial portions of the Software.
 18 
 19  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 20  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 21  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 22  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 23  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 24  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 25  THE SOFTWARE.
 26  ****************************************************************************/
 27 
 28 /**
 29  * the value of stencil bits.
 30  * @type Number
 31  */
 32 cc.stencilBits = -1;
 33 
 34 cc.setProgram = function (node, program) {
 35     node.shaderProgram = program;
 36 
 37     var children = node.children;
 38     if (!children)
 39         return;
 40 
 41     for (var i = 0; i < children.length; i++)
 42         cc.setProgram(children[i], program);
 43 };
 44 
 45 /**
 46  * <p>
 47  *     cc.ClippingNode is a subclass of cc.Node.                                                            <br/>
 48  *     It draws its content (childs) clipped using a stencil.                                               <br/>
 49  *     The stencil is an other cc.Node that will not be drawn.                                               <br/>
 50  *     The clipping is done using the alpha part of the stencil (adjusted with an alphaThreshold).
 51  * </p>
 52  * @class
 53  * @extends cc.Node
 54  *
 55  * @property {Number}   alphaThreshold  - Threshold for alpha value.
 56  * @property {Boolean}  inverted        - Indicate whether in inverted mode.
 57  * @property {cc.Node}  stencil         - he cc.Node to use as a stencil to do the clipping.
 58  */
 59 cc.ClippingNode = cc.Node.extend(/** @lends cc.ClippingNode# */{
 60     alphaThreshold: 0,
 61     inverted: false,
 62 
 63     _stencil: null,
 64     _godhelpme: false,
 65 
 66     /**
 67      * Creates and initializes a clipping node with an other node as its stencil.
 68      * The stencil node will be retained.
 69      * Constructor of cc.ClippingNode
 70      * @param {cc.Node} [stencil=null]
 71      */
 72     ctor: function (stencil) {
 73         cc.Node.prototype.ctor.call(this);
 74         this._stencil = null;
 75         this.alphaThreshold = 0;
 76         this.inverted = false;
 77 
 78         stencil = stencil || null;
 79         cc.ClippingNode.prototype.init.call(this, stencil);
 80     },
 81 
 82     /**
 83      * Initializes a clipping node with an other node as its stencil.                          <br/>
 84      * The stencil node will be retained, and its parent will be set to this clipping node.
 85      * @param {cc.Node} [stencil=null]
 86      */
 87     init: null,
 88     _className: "ClippingNode",
 89 
 90     _initForWebGL: function (stencil) {
 91         this._stencil = stencil;
 92 
 93         this.alphaThreshold = 1;
 94         this.inverted = false;
 95         // get (only once) the number of bits of the stencil buffer
 96         cc.ClippingNode._init_once = true;
 97         if (cc.ClippingNode._init_once) {
 98             cc.stencilBits = cc._renderContext.getParameter(cc._renderContext.STENCIL_BITS);
 99             if (cc.stencilBits <= 0)
100                 cc.log("Stencil buffer is not enabled.");
101             cc.ClippingNode._init_once = false;
102         }
103         return true;
104     },
105 
106     _initForCanvas: function (stencil) {
107         this._stencil = stencil;
108         this.alphaThreshold = 1;
109         this.inverted = false;
110     },
111 
112     onEnter: function () {
113         cc.Node.prototype.onEnter.call(this);
114         this._stencil.onEnter();
115     },
116 
117     onEnterTransitionDidFinish: function () {
118         cc.Node.prototype.onEnterTransitionDidFinish.call(this);
119         this._stencil.onEnterTransitionDidFinish();
120     },
121 
122     onExitTransitionDidStart: function () {
123         this._stencil.onExitTransitionDidStart();
124         cc.Node.prototype.onExitTransitionDidStart.call(this);
125     },
126 
127     onExit: function () {
128         this._stencil.onExit();
129         cc.Node.prototype.onExit.call(this);
130     },
131 
132     visit: null,
133 
134     _visitForWebGL: function (ctx) {
135         var gl = ctx || cc._renderContext;
136 
137         // if stencil buffer disabled
138         if (cc.stencilBits < 1) {
139             // draw everything, as if there where no stencil
140             cc.Node.prototype.visit.call(this, ctx);
141             return;
142         }
143 
144         // return fast (draw nothing, or draw everything if in inverted mode) if:
145         // - nil stencil node
146         // - or stencil node invisible:
147         if (!this._stencil || !this._stencil.visible) {
148             if (this.inverted)
149                 cc.Node.prototype.visit.call(this, ctx);   // draw everything
150             return;
151         }
152 
153         // store the current stencil layer (position in the stencil buffer),
154         // this will allow nesting up to n CCClippingNode,
155         // where n is the number of bits of the stencil buffer.
156 
157         // all the _stencilBits are in use?
158         if (cc.ClippingNode._layer + 1 == cc.stencilBits) {
159             // warn once
160             cc.ClippingNode._visit_once = true;
161             if (cc.ClippingNode._visit_once) {
162                 cc.log("Nesting more than " + cc.stencilBits + "stencils is not supported. Everything will be drawn without stencil for this node and its childs.");
163                 cc.ClippingNode._visit_once = false;
164             }
165             // draw everything, as if there where no stencil
166             cc.Node.prototype.visit.call(this, ctx);
167             return;
168         }
169 
170         ///////////////////////////////////
171         // INIT
172 
173         // increment the current layer
174         cc.ClippingNode._layer++;
175 
176         // mask of the current layer (ie: for layer 3: 00000100)
177         var mask_layer = 0x1 << cc.ClippingNode._layer;
178         // mask of all layers less than the current (ie: for layer 3: 00000011)
179         var mask_layer_l = mask_layer - 1;
180         // mask of all layers less than or equal to the current (ie: for layer 3: 00000111)
181         var mask_layer_le = mask_layer | mask_layer_l;
182 
183         // manually save the stencil state
184         var currentStencilEnabled = gl.isEnabled(gl.STENCIL_TEST);
185         var currentStencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK);
186         var currentStencilFunc = gl.getParameter(gl.STENCIL_FUNC);
187         var currentStencilRef = gl.getParameter(gl.STENCIL_REF);
188         var currentStencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK);
189         var currentStencilFail = gl.getParameter(gl.STENCIL_FAIL);
190         var currentStencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL);
191         var currentStencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS);
192 
193         // enable stencil use
194         gl.enable(gl.STENCIL_TEST);
195         // check for OpenGL error while enabling stencil test
196         //cc.checkGLErrorDebug();
197 
198         // all bits on the stencil buffer are readonly, except the current layer bit,
199         // this means that operation like glClear or glStencilOp will be masked with this value
200         gl.stencilMask(mask_layer);
201 
202         // manually save the depth test state
203         //GLboolean currentDepthTestEnabled = GL_TRUE;
204         //currentDepthTestEnabled = glIsEnabled(GL_DEPTH_TEST);
205         var currentDepthWriteMask = gl.getParameter(gl.DEPTH_WRITEMASK);
206 
207         // disable depth test while drawing the stencil
208         //glDisable(GL_DEPTH_TEST);
209         // disable update to the depth buffer while drawing the stencil,
210         // as the stencil is not meant to be rendered in the real scene,
211         // it should never prevent something else to be drawn,
212         // only disabling depth buffer update should do
213         gl.depthMask(false);
214 
215         ///////////////////////////////////
216         // CLEAR STENCIL BUFFER
217 
218         // manually clear the stencil buffer by drawing a fullscreen rectangle on it
219         // setup the stencil test func like this:
220         // for each pixel in the fullscreen rectangle
221         //     never draw it into the frame buffer
222         //     if not in inverted mode: set the current layer value to 0 in the stencil buffer
223         //     if in inverted mode: set the current layer value to 1 in the stencil buffer
224         gl.stencilFunc(gl.NEVER, mask_layer, mask_layer);
225         gl.stencilOp(!this.inverted ? gl.ZERO : gl.REPLACE, gl.KEEP, gl.KEEP);
226 
227         // draw a fullscreen solid rectangle to clear the stencil buffer
228         cc.kmGLMatrixMode(cc.KM_GL_PROJECTION);
229         cc.kmGLPushMatrix();
230         cc.kmGLLoadIdentity();
231         cc.kmGLMatrixMode(cc.KM_GL_MODELVIEW);
232         cc.kmGLPushMatrix();
233         cc.kmGLLoadIdentity();
234         cc._drawingUtil.drawSolidRect(cc.p(-1,-1), cc.p(1,1), cc.color(255, 255, 255, 255));
235         cc.kmGLMatrixMode(cc.KM_GL_PROJECTION);
236         cc.kmGLPopMatrix();
237         cc.kmGLMatrixMode(cc.KM_GL_MODELVIEW);
238         cc.kmGLPopMatrix();
239 
240         ///////////////////////////////////
241         // DRAW CLIPPING STENCIL
242 
243         // setup the stencil test func like this:
244         // for each pixel in the stencil node
245         //     never draw it into the frame buffer
246         //     if not in inverted mode: set the current layer value to 1 in the stencil buffer
247         //     if in inverted mode: set the current layer value to 0 in the stencil buffer
248         gl.stencilFunc(gl.NEVER, mask_layer, mask_layer);
249         gl.stencilOp(!this.inverted ? gl.REPLACE : gl.ZERO, gl.KEEP, gl.KEEP);
250 
251         if (this.alphaThreshold < 1) {
252             // since glAlphaTest do not exists in OES, use a shader that writes
253             // pixel only if greater than an alpha threshold
254             var program = cc.shaderCache.programForKey(cc.SHADER_POSITION_TEXTURECOLORALPHATEST);
255             var alphaValueLocation = gl.getUniformLocation(program.getProgram(), cc.UNIFORM_ALPHA_TEST_VALUE_S);
256             // set our alphaThreshold
257             cc.glUseProgram(program.getProgram());
258             program.setUniformLocationWith1f(alphaValueLocation, this.alphaThreshold);
259             // we need to recursively apply this shader to all the nodes in the stencil node
260             // XXX: we should have a way to apply shader to all nodes without having to do this
261             cc.setProgram(this._stencil, program);
262         }
263 
264         // draw the stencil node as if it was one of our child
265         // (according to the stencil test func/op and alpha (or alpha shader) test)
266         cc.kmGLPushMatrix();
267         this.transform();
268         this._stencil.visit();
269         cc.kmGLPopMatrix();
270 
271         // restore alpha test state
272         //if (this.alphaThreshold < 1) {
273         // XXX: we need to find a way to restore the shaders of the stencil node and its childs
274         //}
275 
276         // restore the depth test state
277         gl.depthMask(currentDepthWriteMask);
278 
279         ///////////////////////////////////
280         // DRAW CONTENT
281 
282         // setup the stencil test func like this:
283         // for each pixel of this node and its childs
284         //     if all layers less than or equals to the current are set to 1 in the stencil buffer
285         //         draw the pixel and keep the current layer in the stencil buffer
286         //     else
287         //         do not draw the pixel but keep the current layer in the stencil buffer
288         gl.stencilFunc(gl.EQUAL, mask_layer_le, mask_layer_le);
289         gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
290 
291         // draw (according to the stencil test func) this node and its childs
292         cc.Node.prototype.visit.call(this, ctx);
293 
294         ///////////////////////////////////
295         // CLEANUP
296 
297         // manually restore the stencil state
298         gl.stencilFunc(currentStencilFunc, currentStencilRef, currentStencilValueMask);
299         gl.stencilOp(currentStencilFail, currentStencilPassDepthFail, currentStencilPassDepthPass);
300         gl.stencilMask(currentStencilWriteMask);
301         if (!currentStencilEnabled)
302             gl.disable(gl.STENCIL_TEST);
303 
304         // we are done using this layer, decrement
305         cc.ClippingNode._layer--;
306     },
307 
308     _visitForCanvas: function (ctx) {
309         // return fast (draw nothing, or draw everything if in inverted mode) if:
310         // - nil stencil node
311         // - or stencil node invisible:
312         if (!this._stencil || !this._stencil.visible) {
313             if (this.inverted)
314                 cc.Node.prototype.visit.call(this, ctx);   // draw everything
315             return;
316         }
317 
318         var context = ctx || cc._renderContext;
319         var canvas = context.canvas;
320         // Composition mode, costy but support texture stencil
321         if (this._cangodhelpme() || this._stencil instanceof cc.Sprite) {
322             // Cache the current canvas, for later use (This is a little bit heavy, replace this solution with other walkthrough)
323             var locCache = cc.ClippingNode._getSharedCache();
324             locCache.width = canvas.width;
325             locCache.height = canvas.height;
326             var locCacheCtx = locCache.getContext("2d");
327             locCacheCtx.drawImage(canvas, 0, 0);
328 
329             context.save();
330             // Draw everything first using node visit function
331             cc.Node.prototype.visit.call(this, context);
332 
333             context.globalCompositeOperation = this.inverted ? "destination-out" : "destination-in";
334 
335             this.transform(context);
336             this._stencil.visit();
337 
338             context.restore();
339 
340             // Redraw the cached canvas, so that the cliped area shows the background etc.
341             context.save();
342             context.setTransform(1, 0, 0, 1, 0, 0);
343             context.globalCompositeOperation = "destination-over";
344             context.drawImage(locCache, 0, 0);
345             context.restore();
346         }
347         // Clip mode, fast, but only support cc.DrawNode
348         else {
349             var i, children = this._children, locChild;
350 
351             context.save();
352             this.transform(context);
353             this._stencil.visit(context);
354             if (this.inverted) {
355                 context.save();
356 
357                 context.setTransform(1, 0, 0, 1, 0, 0);
358 
359                 context.moveTo(0, 0);
360                 context.lineTo(0, canvas.height);
361                 context.lineTo(canvas.width, canvas.height);
362                 context.lineTo(canvas.width, 0);
363                 context.lineTo(0, 0);
364 
365                 context.restore();
366             }
367             context.clip();
368 
369             // Clip mode doesn't support recusive stencil, so once we used a clip stencil,
370             // so if it has ClippingNode as a child, the child must uses composition stencil.
371             this._cangodhelpme(true);
372             var len = children.length;
373             if (len > 0) {
374                 this.sortAllChildren();
375                 // draw children zOrder < 0
376                 for (i = 0; i < len; i++) {
377                     locChild = children[i];
378                     if (locChild._localZOrder < 0)
379                         locChild.visit(context);
380                     else
381                         break;
382                 }
383                 this.draw(context);
384                 for (; i < len; i++) {
385                     children[i].visit(context);
386                 }
387             } else
388                 this.draw(context);
389             this._cangodhelpme(false);
390 
391             context.restore();
392         }
393     },
394 
395     /**
396      * The cc.Node to use as a stencil to do the clipping.                                   <br/>
397      * The stencil node will be retained. This default to nil.
398      * @return {cc.Node}
399      */
400     getStencil: function () {
401         return this._stencil;
402     },
403 
404     /**
405      * @function
406      * @param {cc.Node} stencil
407      */
408     setStencil: null,
409 
410     _setStencilForWebGL: function (stencil) {
411         this._stencil = stencil;
412     },
413 
414     _setStencilForCanvas: function (stencil) {
415         this._stencil = stencil;
416         var locContext = cc._renderContext;
417         // For texture stencil, use the sprite itself
418         if (stencil instanceof cc.Sprite) {
419             return;
420         }
421         // For shape stencil, rewrite the draw of stencil ,only init the clip path and draw nothing.
422         else if (stencil instanceof cc.DrawNode) {
423             stencil.draw = function () {
424                 var locEGL_ScaleX = cc.view.getScaleX(), locEGL_ScaleY = cc.view.getScaleY();
425                 locContext.beginPath();
426                 for (var i = 0; i < stencil._buffer.length; i++) {
427                     var element = stencil._buffer[i];
428                     var vertices = element.verts;
429 
430                     //cc.assert(cc.vertexListIsClockwise(vertices),
431                     //    "Only clockwise polygons should be used as stencil");
432 
433                     var firstPoint = vertices[0];
434                     locContext.moveTo(firstPoint.x * locEGL_ScaleX, -firstPoint.y * locEGL_ScaleY);
435                     for (var j = 1, len = vertices.length; j < len; j++)
436                         locContext.lineTo(vertices[j].x * locEGL_ScaleX, -vertices[j].y * locEGL_ScaleY);
437                 }
438             }
439         }
440     },
441 
442     /**
443      * <p>
444      * The alpha threshold.                                                                                   <br/>
445      * The content is drawn only where the stencil have pixel with alpha greater than the alphaThreshold.     <br/>
446      * Should be a float between 0 and 1.                                                                     <br/>
447      * This default to 1 (so alpha test is disabled).
448      * </P>
449      * @return {Number}
450      */
451     getAlphaThreshold: function () {
452         return this.alphaThreshold;
453     },
454 
455     /**
456      * set alpha threshold.
457      * @param {Number} alphaThreshold
458      */
459     setAlphaThreshold: function (alphaThreshold) {
460         this.alphaThreshold = alphaThreshold;
461     },
462 
463     /**
464      * <p>
465      *     Inverted. If this is set to YES,                                                                 <br/>
466      *     the stencil is inverted, so the content is drawn where the stencil is NOT drawn.                 <br/>
467      *     This default to NO.
468      * </p>
469      * @return {Boolean}
470      */
471     isInverted: function () {
472         return this.inverted;
473     },
474 
475 
476     /**
477      * set whether or not invert of stencil
478      * @param {Boolean} inverted
479      */
480     setInverted: function (inverted) {
481         this.inverted = inverted;
482     },
483 
484     _cangodhelpme: function (godhelpme) {
485         if (godhelpme === true || godhelpme === false)
486             cc.ClippingNode.prototype._godhelpme = godhelpme;
487         return cc.ClippingNode.prototype._godhelpme;
488     }
489 });
490 
491 var _p = cc.ClippingNode.prototype;
492 
493 if (cc._renderType === cc._RENDER_TYPE_WEBGL) {
494     //WebGL
495     _p.init = _p._initForWebGL;
496     _p.visit = _p._visitForWebGL;
497     _p.setStencil = _p._setStencilForWebGL;
498 } else {
499     _p.init = _p._initForCanvas;
500     _p.visit = _p._visitForCanvas;
501     _p.setStencil = _p._setStencilForCanvas;
502 }
503 
504 // Extended properties
505 cc.defineGetterSetter(_p, "stencil", _p.getStencil, _p.setStencil);
506 /** @expose */
507 _p.stencil;
508 
509 
510 cc.ClippingNode._init_once = null;
511 cc.ClippingNode._visit_once = null;
512 cc.ClippingNode._layer = -1;
513 cc.ClippingNode._sharedCache = null;
514 
515 cc.ClippingNode._getSharedCache = function () {
516     return (cc.ClippingNode._sharedCache) || (cc.ClippingNode._sharedCache = document.createElement("canvas"));
517 };
518 
519 /**
520  * Creates and initializes a clipping node with an other node as its stencil.                               <br/>
521  * The stencil node will be retained.
522  * @deprecated
523  * @param {cc.Node} [stencil=null]
524  * @return {cc.ClippingNode}
525  */
526 cc.ClippingNode.create = function (stencil) {
527     return new cc.ClippingNode(stencil);
528 };
529