Cocos2d-JS Tutorial – Chalk Board 3 – Drawing Grid Lines

Objective

In Continuation of our last Chalk Board Erasing post, let’s see how to automatically draw Grid lines in this post.

ENGINE VERSION : COCOS2D-JS 3.8.1

Preview

Contents

  1. Prerequisite
  2. Download
  3. Draw a Line
  4. Draw smoother Line
  5. Draw vertical Lines
  6. Draw Grid
  7. Center in Board
  8. Complete Board.js Code

At any point of time, refer to the source files Board.jsmain.js and resource.js for clarity.

1. Prerequisite

See this link to get started with Cocos2d-x.

2. Download Code

Download project @ https://github.com/GethuGames/cocos2dx-chalk-board-draw/archive/grid1.0.zip Complete Project can be found in GITHUB.

Note: This is not directly executable. This contains the source and resources excluding the cocos2dx bundle. So, create a new project, download this github and replace the files in ur new project with this github files to be able to run the project.

3. Draw a Line

Create a class variable `currentLinePercent` and 2 functions, `drawBoard` and `updateDrawLine`.

  1. currentLinePercent:The line will be drawn by a series of dots and this variable determines where the dot should be drawn at any given time. currentLinePercent will start at 1 and gradually decreased to 0 over time in the `update` function.
  2. drawBoard: This function will be invoked from the Class constructor. This will just schedule the other function `updateDrawLine` that has the core logic.
  3. updateDrawLine: This will be invoked in every game cycle. This function will decrement currentLinePercent by a small amount, then calculate the point to be drawn, and call `drawBrushAtPoint()` at that point. This will be repeated till currentLinePercent reaches 0, and hence the line touches the bottom of the screen.
    drawBoard : function() {
        this.currentLinePercent = 1.0;
        this.schedule(this.updateDrawLine);
    },

    updateDrawLine : function(dt) {
        this.currentLinePercent -=  0.02;
        to = cc.p(cc.winSize.width * 0.5, this.currentLinePercent * cc.winSize.height);
        this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200));

        if (this.currentLinePercent < 0.0) {
            this.unschedule(this.updateDrawLine);
        }
    },
  1. Schedule the `updateDrawLine` to be invoked on every game loop.
  2. Line’s Y position is calculated from currentLinePercent,
    1. which is set to 1 in the beginning (= max height, top of the screen)
    2. and decremented to 0 over time (bottom of the screen)
  3. Point to draw is calculated as follows:
    1. X position: the center of the screen
    2. Y position: currentLinePercent * window height
  4. When the currentLinePercent reaches 0, which is the bottom of the screen, unschedule `updateDrawLine` and stop Drawing.

As with the free hand drawing in Part 1, here also, we get gaps between the points. We can interpolate the intermediate points as before. But just for the sake learning/exploration, let’s take another technique.

Line draw - First

4. Draw smoother Line

Since we know the lines are perfectly vertical, we can generate fixed number of points above and below the given point and draw them all. So, `drawBrustAtPoint()` changes to

    drawBrushAtPoint : function(pt, color, density) {
        this.renderTex.begin();
        for (var i = 0; i < density; i++) {
            this.chalkBrush = cc.Sprite.create(res.chalkBrush_png);
            this.chalkBrush.setRotation(Math.random() * 180);
            this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
            this.chalkBrush.setColor(color);
            this.chalkBrush.setScale(1.5);
            this.chalkBrush.visit();
        }
        this.renderTex.end();

        this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
            pt.y - this.chalkSprite.getContentSize().height * 0.2));
    }

We introduced a new parameter `density` and a `for` loop in the function.

  • Density determines how many points are drawn above and below to the given point. If density = 12, 6 points are drawn above and 6 points drawn below the given point.
  • For loop iterates `density` times, generating points with a gap of 5 pixels from each other.

Then in `updateDrawLine()`, change

this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200));

to

this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200), 16);

which means, we now draw 8 points above and below the given point `to`.

This should give us a smooth continuous line as this:

Line draw - Second

5. Draw vertical lines

We mastered drawing a single vertical line. Now let’s draw multiple vertical lines of the grid. Before starting to draw the lines, let’s create a `Config` object to make certain things simpler and configurable.

var Config = {};

Add this as the first line before the `Board` declaration. Then in the Board constructor, specify the properties of Config as follows:

Config.colCount         =   5;  // Number of columns in the grid
Config.rowCount         =   5;  // Number of rows in the grid
Config.drawSpeed        =   10; // Speed of Chalk Drawing
Config.lineColor        =   cc.color(250, 250, 50, 200); // Grid color

We dont want to draw in the full Board. But rather, let’s draw a square shapred box. To achieve that, add these 2 lines of code in the Board constructor:

this.boardSpace         =   cc.size(size.width * 0.75, size.width * 0.75);
this.cellSize           =   cc.size(this.boardSpace.width / Config.colCount, this.boardSpace.height / Config.rowCount);
  • boardSpace: This is pre-calculated area of the grid, which is now 75% of the width of the board.
  • cellSize: This is the size of each and every cell in the grid. It is calculated by dividing the boardSpace by row / column count.

Now we will replace the `updateDrawLine()` with a little more complex `updateVGridDraw()`. This will first draw a vertical line. Once it is over, it will increment x by some value and draw another vertical line. Keeps repeating this logic for `Config.colCount + 1` times. Here is the code for `updateVGridDraw()`

this.vLineIndex  = 0; // add this line in `drawBoard` function

updateVGridDraw              :   function(dt) {
    this.currentLinePercent         -=  0.01 * Config.drawSpeed;
    to                  =   cc.p(this.vLineIndex * this.cellSize.width, this.currentLinePercent * this.boardSpace.height);
    this.drawBrushAtPoint(to, Config.lineColor, 16);

    if (this.currentLinePercent < 0.01) {
        this.vLineIndex++;
        if (this.vLineIndex <= Config.colCount) {
            this.currentLinePercent = 1.0;
        } else {
            this.unschedule(this.updateVGridDraw);
        }
    }
}
  • vLineIndex: When drawing multiple lines, this variable keeps track of which line is being currently drawn. This also used to calculate the x position of the current line as in the following table.
    Table 1

    vLineIndex xPosition
    0 0
    1 cellSize.width
    2 cellSize.width * 2
    3 cellSize.width * 3
    n cellSize.width * n
  • Config.drawSpeed: This is a multiplier that determines by what percetange the `currentLinePercent` is getting changed. The more value it is, the point will be drawn at big distances and quickly.
  • Draw Point `to`: The draw point is then calculated with `vLineIndex` and `currentLinePercent`. For y position, rather than drawing to full board height, now we are only covering boardSpace.height, which is 75% of the width of the board.
  • We then call the `drawBrushAtPoint()` with calculated point `to`, which draws the dot with a given density of 16.
  • Other lines: The final `If` block is to draw further lines.
    • We check if the current line drawing is over, by checking if currentLinePercent reached 0.
    • When it is, we increment vLineIndex by 1 and reset currentLinePercent to 1. So a new line will get drawn but at the next x position.
    • When the vLineIndex reaches `Config.colCount`, we stop unschedule `updateVGridDraw()`, thus stopping the whole line drawing.

Vertical Lines Draw

6. Draw Grid

Now that we know how to draw Vertical lines, we can replicate the same and do it horizontally. Code update to draw horizontal lines.

updateHGridDraw              :   function(dt) {
    this.currentLinePercent         +=  0.01 * Config.drawSpeed;
    if (this.currentLinePercent > 0.93)
        this.currentLinePercent     =   0.931;
    to                  =   cc.p(this.currentLinePercent * this.boardSpace.width, this.hLineIndex * this.cellSize.height);
    this.drawBrushAtPoint(to, Config.lineColor, 16, false);

    if (this.currentLinePercent > 0.93) {
        this.hLineIndex--;
        if (this.hLineIndex != -1) {
            this.currentLinePercent =   -0.01;
        } else {
            this.unschedule(this.updateHGridDraw);
        }
    }
}

// update else block of `updateVGridDraw` with this code:
this.vLineIndex         =   -1;
this.hLineIndex         =   Config.rowCount;
this.currentLinePercent =   -0.01;
this.unschedule(this.updateVGridDraw);
this.schedule(this.updateHGridDraw, 0);

// update `drawBrushAtPoint` with this code:
drawBrushAtPoint            :   function(pt, color, density, vertical) { //new parameter vertical

// replace the line `this.chalkBrush.setPosition(...)` with this block inside `drawBrushAtPoint` function
if (vertical) {
    this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
} else {
    this.chalkBrush.setPosition(cc.p(pt.x + 5 * (i - density * 0.5), pt.y));
} 
  • `updateVGridDraw()` is very similar to `updateHGridDraw()`, except the x position is calculated by varying currentLinePercent and y position calculated by hLineIndex.
  • `drawBrushAtPoint()` gets a new boolean parameter vertical.
    • When vertical is true, adjacent filling dots are calculated vertically as in updateVGridDraw().
    • When vertical is false, adjacent filling dots are calculated horizontally as in updateHGridDraw().
  • Then the else block of updateVGridDraw() is replaced with above 5 lines of code, which unschedules the vertical line draw function, updateVGridDraw() and schedules the horizontal line draw function, updateHGridDraw().

At this point, we should be getting the output as follows:

Grid Lines Draw

7. Center in Board

The grid is now neatly drawn. But wait, it’s drawn over the edge wood portion. We need to bring it to the center of the board to make it look perfect. Considering what we done till now, centering the grid is just a piece of cake. Here is the logic:

Since the board size is 75% of the width, moving the board to 12.5% of the width will center the grid. So our layout would be like this.

|    space   |        board         |    space    |
      12%              75%                12%

To do this, create a new variable `offset` in the constructor as follows:

this.offset = cc.p(size.width * 0.12, size.height * 0.12);

Then update the point (to) calculation line in updateVGridDraw() and updateHGridDraw() as follows:

// updateVGridDraw()
to = cc.p(this.vLineIndex * this.cellSize.width + this.offset.x, this.currentLinePercent * this.boardSpace.height + this.offset.y);

// updateHGridDraw()
to = cc.p(this.currentLinePercent * this.boardSpace.width + this.offset.x, this.hLineIndex * this.cellSize.height + this.offset.y);

This will move our awesome Grid to the center.

Now you can tinker with the properties of Config object to draw grid of various sizes (7×7, 9×9), different chalk color and varying draw speed.

Center Grid Draw

Happy Chalking (y)

8. Complete Board.js Code

var Config                      =   {};

var Board = cc.Scene.extend({

    boardSprite                 :   null,

    renderTex                   :   null,

    chalkSprite                 :   null,

    chalkBrush                  :   null,

    prevPoint                   :   cc.p(0, 0),

    boardSpace                  :   null,

    cellSize                    :   null,

    offset                      :   null,

    mode                        :   0,

    ctor                        :   function () {

        this._super();

        var slf                 =   this;
        var size                =   cc.winSize;
        
        if( 'touches' in cc.sys.capabilities ) { 
            console.log('Can Touch');
            this._touchListener = cc.EventListener.create({
                event: cc.EventListener.TOUCH_ALL_AT_ONCE,
                onTouchesBegan: this.onTouchesBegan,
                onTouchesMoved: this.onTouchesMoved,
                onTouchesEnded: this.onTouchesEnded
            });

            cc.eventManager.addListener(this._touchListener, this);
        } else {
            console.log('No Touch Capabs');
        }

        Config.colCount         =   5;
        Config.rowCount         =   5;
        Config.drawSpeed        =   10;
        Config.lineColor        =   cc.color(250, 250, 50, 200);

        this.boardSpace         =   cc.size(size.width * 0.75, size.width * 0.75);
        this.cellSize           =   cc.size(this.boardSpace.width / Config.colCount, this.boardSpace.height / Config.rowCount);
        this.offset             =   cc.p(size.width * 0.12, size.height * 0.12);

        this.boardSprite        =   cc.Sprite.create(res.Board_BG);
        this.boardSprite.setPosition(cc.p(size.width / 2, size.height / 2));
        this.boardSprite.setScale(640 / this.boardSprite.getContentSize().width,
                size.height / this.boardSprite.getContentSize().height);
        this.addChild(this.boardSprite);

        this.renderTex     =   cc.RenderTexture.create(size.width, size.height);
        this.renderTex.setPosition(cc.p(size.width / 2, size.height / 2));
        this.addChild(this.renderTex);

        this.chalkSprite        =   cc.Sprite.create(res.chalk_png);
        this.chalkSprite.setPosition(cc.p(size.width * 0.7, this.chalkSprite.getContentSize().height * 0.8));
        this.addChild(this.chalkSprite, 2);

        this.duster             =   cc.Sprite.create(res.duster_png);
        this.duster.setPosition(cc.p(this.duster.getContentSize().width * 0.5, this.duster.getContentSize().height * 0.8));
        this.duster.setScale(0.5);
        this.duster.setRotation(30);
        this.addChild(this.duster, 2);

        this.drawBoard();
    }, 

    onTouchesBegan:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Began : ' + JSON.stringify(pos));
        
        slf.prevPoint           =   pos;
        slf.mode                =   0;

        dist                    =   Math.round(cc.pDistance(pos, slf.duster.getPosition()));
        if (dist < slf.duster.getContentSize().width) {
            slf.mode            =   1;
        }

        return                      true;
    },

    onTouchesMoved:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Move : ' + JSON.stringify(pos));
        //console.log('PrevPoint : ' + JSON.stringify(slf.prevPoint));

        var dist                =   Math.round(cc.pDistance(pos, slf.prevPoint));
        //console.log(dist);

        for (var i = 0; i < dist; i += 5) {
            var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
            if (slf.mode == 0) {
                slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230), 1, true);
            } else if (slf.mode == 1) {
                slf.eraseAtPoint(cPos);
            }
        }

        slf.prevPoint           =   pos;
    },

    onTouchesEnded              :   function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        var size                =   cc.winSize;

        console.log('End : ' + JSON.stringify(pos));

        if (slf.mode == 0) {
            slf.chalkSprite.runAction(cc.moveTo(0.2, cc.p(size.width * 0.7, slf.chalkSprite.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        } else if (slf.mode == 1) {
            slf.duster.runAction(cc.moveTo(0.2, cc.p(slf.duster.getContentSize().width * 0.5, slf.duster.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        }

    },

    drawBrushAtPoint            :   function(pt, color, density, vertical) {

        this.renderTex.begin();
        for (var i = 0; i < density; i++) {
            this.chalkBrush     =   cc.Sprite.create(res.chalkBrush_png);
            this.chalkBrush.setRotation(Math.random() * 180);
            if (vertical) {
                this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
            } else {
                this.chalkBrush.setPosition(cc.p(pt.x + 5 * (i - density * 0.5), pt.y));
            }
            this.chalkBrush.setColor(color);
            this.chalkBrush.setScale(1.5);
            this.chalkBrush.visit();
        }
        this.renderTex.end();

        this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
            pt.y - this.chalkSprite.getContentSize().height * 0.2));

    },

    eraseAtPoint                :   function(pt) {

        this.renderTex.begin();
        this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
        this.chalkBrush.setRotation(Math.random() * 180);
        this.chalkBrush.setPosition(pt);
        this.chalkBrush.setScale(10.0);
        this.chalkBrush.setOpacity(230);
        this.chalkBrush.setBlendFunc(  cc.ZERO, cc.ONE_MINUS_SRC_ALPHA );
        this.chalkBrush.visit();
        this.renderTex.end();

        this.duster.setPosition(pt);

    },

    drawBoard                   :   function(dt) {

        this.vLineIndex         =   0;
        this.hLineIndex         =   -1;
        this.currentLinePercent =   1.025;

        this.schedule(this.updateVGridDraw, 0);
    },

    updateVGridDraw              :   function(dt) {

        this.currentLinePercent         -=  0.01 * Config.drawSpeed;
        if (this.currentLinePercent < 0.08) 
            this.currentLinePercent = 0.079;
        to                  =   cc.p(this.vLineIndex * this.cellSize.width + this.offset.x, this.currentLinePercent * this.boardSpace.height + this.offset.y);
        this.drawBrushAtPoint(to, Config.lineColor, 16, true);

        if (this.currentLinePercent < 0.08) {
            this.vLineIndex++;
            if (this.vLineIndex <= Config.colCount) { this.currentLinePercent = 1.025; } else { this.vLineIndex = -1; this.hLineIndex = Config.rowCount; this.currentLinePercent = -0.01; this.unschedule(this.updateVGridDraw); this.schedule(this.updateHGridDraw, 0); } } }, updateHGridDraw : function(dt) { this.currentLinePercent += 0.01 * Config.drawSpeed; if (this.currentLinePercent > 0.93)
            this.currentLinePercent     =   0.931;
        to                  =   cc.p(this.currentLinePercent * this.boardSpace.width + this.offset.x, this.hLineIndex * this.cellSize.height + this.offset.y);
        this.drawBrushAtPoint(to, Config.lineColor, 16, false);

        if (this.currentLinePercent > 0.93) {
            this.hLineIndex--;
            if (this.hLineIndex != -1) {
                this.currentLinePercent =   -0.01;
            } else {
                this.unschedule(this.updateHGridDraw);
            }
        }
    }


});

Cocos2d-JS Tutorial – Chalk Board 2 – Erasing

Objective

In Continuation of our last Chalk Board drawing post, let’s see how to erase the board in this post.

ENGINE VERSION : COCOS2D-JS 3.8.1

Preview

Contents

  1. Prerequisite
  2. Download
  3. Chalk back to Position
  4. Draw / Erase Mode
  5. Touch Eraser Detection
  6. Erasing Magic
  7. Complete Board.js Code

At any point of time, refer to the source files Board.jsmain.js and resource.js for clarity.

1. Prerequisite

  • Should have Cocos2d-x installed in system.
  • Should have basic working knowledge of Cocos2d-JS
  • Read Previous post on Chalk Board Drawing.

See this link to get started with Cocos2d-x.

2. Download Code

Download project @ https://github.com/GethuGames/cocos2dx-chalk-board-draw/archive/erase1.0.zip Complete Project can be found in GITHUB.

Note: This is not directly executable. This contains the source and resources excluding the cocos2dx bundle. So, create a new project, download this github and replace the files in ur new project with this github files to be able to run the project.

3. Chalk back to Position

When you are erasing, you dont want the chalk to be in the middle of the board and overlap with the Eraser. So let’s create a action that will move the chalk to the bottom of the move after drawing (on touches ended).

// inside `onTouchesEnded` of Board.js
slf.chalkSprite.runAction(cc.moveTo(0.2, cc.p(size.width * 0.7, slf.chalkSprite.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));

Add that code in `onTouchesEnded`, so as to drop the chalk to bottom after each draw.

4. Draw / Erase Mode

As you have seen in the video above, we should be able to drag the Eraser to clear the board. So to separate erasing and drawing in the common action of dragging, lets define a variable `mode`.

     mode : 0,
  1. When mode is 0, our Canvas will be in drawing mode and
  2. When it is 1, it will be in Erasing mode.
  3. When the touch begins on Eraser, we set the mode to 1 and drag the eraser instead of chalk.
  4. Touching anywhere else other than Eraser, will set the mode to 0 and start drawing.

5. Touch Eraser Detection

Now in `touchesBegan`, we need to identify If the touch is made on Eraser and set the mode accordingly.

// This code goes in `onTouchesBegan` function of Board.js
        slf.mode                =   0;

        dist                    =   Math.round(cc.pDistance(pos, slf.duster.getPosition()));
        if (dist < slf.duster.getContentSize().width) {
            slf.mode            =   1;
        }

Above is a very simple Hit Test Logic.

  • Set the mode to 0 (Draw) by default.
  • Then find the distance between touch point (pos) and eraser.
  • If the distance is less than the size of the eraser itself, then set the mode to `1`, since the touch is made on or very close to the Eraser.

Also introduce a `if` condition inside the `for` loop of `onTouchesMoved`:

        for (var i = 0; i < dist; i += 5) {
            var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
            if (slf.mode == 0) {
                slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230));
            } else if (slf.mode == 1) {
                // Erase Code to be updated
            }
        }

Now, drawing lines will be called only when the mode is `0`. Try running the code and see what happens when you touch on Eraser and drag across the board. You will see no lines.

All pieces are now set. Let’s see how to do the actual erasing Magic.

6. Erasing Magic

Now close the function `drawBrushAtPoint()` to `eraseAtPoint`. All code will be similar to drawing, except for just one line, which is,

    this.chalkBrush.setBlendFunc( cc.ZERO, cc.ONE_MINUS_SRC_ALPHA );

Setting the blend function of the sprite to ( cc.ZERO, cc.ONE_MINUS_SRC_ALPHA ) will erase rather than drawing. That’s it. Very very simple. Here is the full code of `eraseAtPoint`.

    eraseAtPoint                :   function(pt) {
        this.renderTex.begin();
        this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
        this.chalkBrush.setRotation(Math.random() * 180);
        this.chalkBrush.setPosition(pt);
        this.chalkBrush.setScale(10.0);
        this.chalkBrush.setOpacity(230);
        this.chalkBrush.setBlendFunc(  cc.ZERO, cc.ONE_MINUS_SRC_ALPHA );
        this.chalkBrush.visit();
        this.renderTex.end();

        this.duster.setPosition(pt);
    }

See this is much similar to Drawing code, except for setting the Blend function. With different blend functions, some super awesome visuals can be created.

Erase Line

Happy Coding (y)

Complete Board.js Code

var Board = cc.Scene.extend({

    boardSprite                 :   null,

    renderTex                   :   null,

    chalkSprite                 :   null,

    chalkBrush                  :   null,

    prevPoint                   :   cc.p(0, 0),

    mode                        :   0,

    ctor                        :   function () {

        this._super();

        var slf                 =   this;
        var size                =   cc.winSize;
        
        if( 'touches' in cc.sys.capabilities ) { 
            console.log('Can Touch');
            this._touchListener = cc.EventListener.create({
                event: cc.EventListener.TOUCH_ALL_AT_ONCE,
                onTouchesBegan: this.onTouchesBegan,
                onTouchesMoved: this.onTouchesMoved,
                onTouchesEnded: this.onTouchesEnded
            });

            cc.eventManager.addListener(this._touchListener, this);
        } else {
            console.log('No Touch Capabs');
        }

        this.boardSprite        =   cc.Sprite.create(res.Board_BG);
        this.boardSprite.setPosition(cc.p(size.width / 2, size.height / 2));
        this.boardSprite.setScale(640 / this.boardSprite.getContentSize().width,
                size.height / this.boardSprite.getContentSize().height);
        this.addChild(this.boardSprite);

        this.renderTex     =   cc.RenderTexture.create(size.width, size.height);
        this.renderTex.setPosition(cc.p(size.width / 2, size.height / 2));
        this.addChild(this.renderTex);

        this.chalkSprite        =   cc.Sprite.create(res.chalk_png);
        this.chalkSprite.setPosition(cc.p(size.width * 0.7, this.chalkSprite.getContentSize().height * 0.8));
        this.addChild(this.chalkSprite, 2);

        this.duster             =   cc.Sprite.create(res.duster_png);
        this.duster.setPosition(cc.p(this.duster.getContentSize().width * 0.5, this.duster.getContentSize().height * 0.8));
        this.duster.setScale(0.5);
        this.duster.setRotation(30);
        this.addChild(this.duster, 2);
    }, 

    onTouchesBegan:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Began : ' + JSON.stringify(pos));
        
        slf.prevPoint           =   pos;
        slf.mode                =   0;

        /*
        var dist                =   Math.round(cc.pDistance(pos, slf.chalkSprite.getPosition()));
        if (dist < slf.chalkSprite.getContentSize().width) {
            slf.mode            =   0;
        }
        */

        dist                    =   Math.round(cc.pDistance(pos, slf.duster.getPosition()));
        if (dist < slf.duster.getContentSize().width) {
            slf.mode            =   1;
        }

        return                      true;
    },

    onTouchesMoved:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Move : ' + JSON.stringify(pos));
        //console.log('PrevPoint : ' + JSON.stringify(slf.prevPoint));

        var dist                =   Math.round(cc.pDistance(pos, slf.prevPoint));
        //console.log(dist);

        for (var i = 0; i < dist; i += 5) {
            var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
            if (slf.mode == 0) {
                slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230));
            } else if (slf.mode == 1) {
                slf.eraseAtPoint(cPos);
            }
        }

        slf.prevPoint           =   pos;
    },

    onTouchesEnded              :   function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        var size                =   cc.winSize;

        console.log('End : ' + JSON.stringify(pos));

        if (slf.mode == 0) {
            slf.chalkSprite.runAction(cc.moveTo(0.2, cc.p(size.width * 0.7, slf.chalkSprite.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        } else if (slf.mode == 1) {
            slf.duster.runAction(cc.moveTo(0.2, cc.p(slf.duster.getContentSize().width * 0.5, slf.duster.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        }
                //this.duster.runAction(cc.moveTo(0.1, this.erasePoint).easing(cc.easeIn(1.0)));

    },

    drawBrushAtPoint            :   function(pt, color) {

        this.renderTex.begin();
        this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
        this.chalkBrush.setRotation(Math.random() * 180);
        this.chalkBrush.setPosition(pt);
        this.chalkBrush.setColor(color);
        this.chalkBrush.setScale(1.5);
        this.chalkBrush.visit();
        this.renderTex.end();

        this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
            pt.y - this.chalkSprite.getContentSize().height * 0.2));

    },

    eraseAtPoint                :   function(pt) {

        this.renderTex.begin();
        this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
        this.chalkBrush.setRotation(Math.random() * 180);
        this.chalkBrush.setPosition(pt);
        this.chalkBrush.setScale(10.0);
        this.chalkBrush.setOpacity(230);
        this.chalkBrush.setBlendFunc(  cc.ZERO, cc.ONE_MINUS_SRC_ALPHA );
        this.chalkBrush.visit();
        this.renderTex.end();

        this.duster.setPosition(pt);

    }

});

Cocos2d-JS Tutorial – Chalk Board 1 – Drawing and Animations

A Comeback post after around 2 years :-)

Objective

In this post, we will see how to create a Cool Chalk board drawing functionality in Cocos2d-JS with RenderTexture. Later we will also see how to erase the Board with the Eraser.

Engine Version : Cocos2d-JS 3.8.1

Preview

Contents

  1. Prerequisite
  2. Resources
  3. Download Code
  4. Setup the Chalk Board
  5. Setup Touch functions
  6. Basic Drawing (Dotted)
  7. Smooth Drawing
  8. Complete Board.js Code

At any point of time, refer to the source files Board.jsmain.js and resource.js for clarity

1. Prerequisite

  • Should have Cocos2d-x installed in system.
  • Should have basic working knowledge of Cocos2d-JS

See this link to get started with Cocos2d-x.

2. Resources

Download and Copy these to the `res/` folder.

3. Download Code

Download project @ https://github.com/GethuGames/cocos2dx-chalk-board-draw/archive/draw1.0.zip Complete Project can be found in GITHUB.

Note: This is not directly executable. This contains the source and resources excluding the cocos2dx bundle. So, create a new project, download this github and replace the files in ur new project with this github files to be able to run the project.

4. Setup the Chalk Board

  • Create a Scene `Board`
  • In the constructor of `Board` scene, add the sprites for Chalk Board, Chalk Piece and Eraser as follows:
// This code goes in `ctor` function of Board.js
var size = cc.winSize;

this.boardSprite = cc.Sprite.create(res.Board_BG);
this.boardSprite.setPosition(cc.p(size.width / 2, size.height / 2));
this.boardSprite.setScale(640 / this.boardSprite.getContentSize().width,
size.height / this.boardSprite.getContentSize().height);
this.addChild(this.boardSprite);

this.chalkSprite = cc.Sprite.create(res.chalk_png);
this.chalkSprite.setPosition(cc.p(100, 200));
this.addChild(this.chalkSprite, 2);

this.duster = cc.Sprite.create(res.duster_png);
this.duster.setPosition(cc.p(this.duster.getContentSize().width * 0.5, this.duster.getContentSize().height * 0.8));
this.duster.setScale(0.5);
this.duster.setRotation(30);
this.addChild(this.duster, 2);
  • Then add the `RenderTexture` which will act the real chalk board to draw with the following code.
// This code goes in `ctor` function of Board.js
this.renderTex     =   cc.RenderTexture.create(size.width, size.height);
this.renderTex.setPosition(cc.p(size.width / 2, size.height / 2));
this.addChild(this.renderTex);

Then update the `main.js` to run the `Board` scene:

// This code goes in `onStart` function of main.js
cc.view.setDesignResolutionSize(640, 960, cc.ResolutionPolicy.FIXED_WIDTH);

cc.director.runScene(new Board());

This should display the Chalk Board.

Board Setup

5. Setup Touch Functions

Again in the constructor create Event Listeners to capture touch Began, Move and End.

// This code goes in `ctor` function of Board.js
if( 'touches' in cc.sys.capabilities ) { 
    console.log('Can Touch');
    this._touchListener = cc.EventListener.create({
        event: cc.EventListener.TOUCH_ALL_AT_ONCE,
        onTouchesBegan: this.onTouchesBegan,
        onTouchesMoved: this.onTouchesMoved,
        onTouchesEnded: this.onTouchesEnded
    });
    cc.eventManager.addListener(this._touchListener, this);
} else {
    console.log('No Touch Capabs');
}

Now add touch handlers to the `Board` scene

onTouchesBegan:function(touches, event) {
    var pos                 =   touches[0].getLocation();
    console.log('Began : ' + JSON.stringify(pos));

    return                      true;
},

onTouchesMoved:function(touches, event) {
    var pos                 =   touches[0].getLocation();
    console.log('Move : ' + JSON.stringify(pos));
},

onTouchesEnded              :   function(touches, event) {
    var pos                 =   touches[0].getLocation();
    console.log('End : ' + JSON.stringify(pos));
}

If this is set right, the app should throw logs in the console on touch and drag.

Chalk Board with Console log

6. Basic Drawing (Dotted)

Let’s create a new function that will handle the actual drawing.

drawBrushAtPoint            :   function(pt, color) {
    this.renderTex.begin(); //a
    this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png); //b
    this.chalkBrush.setRotation(Math.random() * 180); //c
    this.chalkBrush.setPosition(pt); //d
    this.chalkBrush.setColor(color); //e
    this.chalkBrush.visit(); //f
    this.renderTex.end(); //g

    this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
        pt.y - this.chalkSprite.getContentSize().height * 0.2)); //h

}

This is where the magic happens. Let’s go through this code line by line.
a : Prepares the Render Texture for Drawing.
b : Creates the brush texture sprite.
c, d, e : Set’s the rotation (random), position (as given) and color(as given) of the texture.
f : Draws the brush texture in the render texture.
g : Closes the Render Texture
h : Set’s the Chalk Sprite’s position to given position

We will call this function from touchesMoved passing the current touch Position and the color of the chalk.

onTouchesMoved:function(touches, event) {
    var slf                 =   event.getCurrentTarget();
    var pos                 =   touches[0].getLocation();
    slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230));
},

Now, you should be able to draw in the canvas by dragging over it.

Preview:

Dotted Draw

But the chalk line is not continuous and when drag faster, it dots the board rather than drawing lines. It’s because the touchesMoved is invoked between distant points when you move faster. Like when you drag really faster, you are at (50, 50) in one cycle and in (100, 150) in the next. Thus drawing 2 dots, one at (50, 50) and other at (100, 150).

No worries, we can fix this with some techniques.

7. Smooth Drawing

To get the drawing smoother, we will interpolate points between two distant `touch move` points and draw there. To do this, update `touchesMoved` function with the following:

onTouchesMoved:function(touches, event) {
    var slf                 =   event.getCurrentTarget();
    var pos                 =   touches[0].getLocation();
    console.log('Move : ' + JSON.stringify(pos));

    var dist                =   Math.round(cc.pDistance(pos, slf.prevPoint)); //A

    for (var i = 0; i < dist; i += 5) { //B
        var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
        slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230));
    }

    slf.prevPoint           =   pos;
},
  • At the end of each `touchesMove` function, we store the point as `prevPoint`.
  • Then on next call of `touchesMove`, we get the distance between current Move position and previous move position (Line A)
  • We create a for loop that will advance 5 pixels from Zero to the distance between 2 points.
  • Inside the loop, we interpolate the corresponding point between the previous point and current points and draw the chalk brush there.
  • This way we will fill the gap between the dots and get a smooth Drawing..

Smooth Draw

Hope this Tutorial is useful :-)

Next Tuturial will be on erasing the board. Subscribe to get Notified.

Happy Coding (y)

Complete Board.js Code

var Board = cc.Scene.extend({

    boardSprite                 :   null,

    renderTex                   :   null,

    chalkSprite                 :   null,

    chalkBrush                  :   null,

    prevPoint                   :   cc.p(0, 0),

    ctor                        :   function () {

        this._super();

        var slf                 =   this;
        var size                =   cc.winSize;
        
        if( 'touches' in cc.sys.capabilities ) { 
            console.log('Can Touch');
            this._touchListener = cc.EventListener.create({
                event: cc.EventListener.TOUCH_ALL_AT_ONCE,
                onTouchesBegan: this.onTouchesBegan,
                onTouchesMoved: this.onTouchesMoved,
                onTouchesEnded: this.onTouchesEnded
            });

            cc.eventManager.addListener(this._touchListener, this);
        } else {
            console.log('No Touch Capabs');
        }

        this.boardSprite        =   cc.Sprite.create(res.Board_BG);
        this.boardSprite.setPosition(cc.p(size.width / 2, size.height / 2));
        this.boardSprite.setScale(640 / this.boardSprite.getContentSize().width,
                size.height / this.boardSprite.getContentSize().height);
        this.addChild(this.boardSprite);

        this.renderTex     =   cc.RenderTexture.create(size.width, size.height);
        this.renderTex.setPosition(cc.p(size.width / 2, size.height / 2));
        this.addChild(this.renderTex);

        this.chalkSprite        =   cc.Sprite.create(res.chalk_png);
        this.chalkSprite.setPosition(cc.p(100, 200));
        this.addChild(this.chalkSprite, 2);

        this.duster             =   cc.Sprite.create(res.duster_png);
        this.duster.setPosition(cc.p(this.duster.getContentSize().width * 0.5, this.duster.getContentSize().height * 0.8));
        this.duster.setScale(0.5);
        this.duster.setRotation(30);
        this.addChild(this.duster, 2);
    }, 

    onTouchesBegan:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        
        slf.prevPoint           =   pos;

        return                      true;
    },

    onTouchesMoved:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        //console.log('Move : ' + JSON.stringify(pos));
        //console.log('PrevPoint : ' + JSON.stringify(slf.prevPoint));

        var dist                =   Math.round(cc.pDistance(pos, slf.prevPoint));
        //console.log(dist);

        for (var i = 0; i < dist; i += 5) {
            var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
            slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230));
        }

        slf.prevPoint           =   pos;
    },

    onTouchesEnded              :   function(touches, event) {
        var pos                 =   touches[0].getLocation();
        console.log('End : ' + JSON.stringify(pos));

    },

drawBrushAtPoint            :   function(pt, color) {

    this.renderTex.begin();
    this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
    this.chalkBrush.setRotation(Math.random() * 180);
    this.chalkBrush.setPosition(pt);
    this.chalkBrush.setColor(color);
    this.chalkBrush.setScale(1.5);
    this.chalkBrush.visit();
    this.renderTex.end();

    this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
        pt.y - this.chalkSprite.getContentSize().height * 0.2));

}

});

Cocos2dx – Jedi’s Light Saber

Lets try creating a stunning Jedi’s Light Saber in this tutorial.

Downloads and Source

Project File (ZIP)

Project @ Github: https://github.com/saiy2k/cocos2dx-lightsaber

1. Create new Cocos2DX Project

Refer to official wiki on creating a new cocos2dx project.

2. `LightSaber` Class

  1. Create a C++ class named `LightSaber` that inherits from `CCLayer`
    //LightSaber.h
    class LightSaber : public CCLayer {
    public:
    
        // MEMBER VARIABLES
        // ****************
        CCSprite                        *textureSprite;
        CCArray                         *saberArray;
    
        // OBJECT LIFE CYCLE
        // *****************
        virtual bool                    init();
        CREATE_FUNC(LightSaber);
        ~LightSaber();
    
    };

    `textureSprite` will hold the texture to be used by the particle system and `saberArray` will hold the lightSaber particle systems. All particle systems in this array will be exact copies of each other, but together they intensify the effect.

  2. The `init` function and the `destructor` allocates memory to the `saberArray` and releases it.
    bool LightSaber::init() {
        if ( !CCLayer::init() ) return false;
    
        saberArray                  =   CCArray::create();
        saberArray->retain();
        return                          true;
    }
    
    LightSaber::~LightSaber() {
        saberArray->release();
    }
  3. Create an Instance of `LightSaber` and it to the running scene in `AppDelegate` as follows:
        // in applicationDidFinishLaunching() of AppDelegate.cpp
        CCScene *pScene = CCScene::create();
        pScene->addChild(LightSaber::create());
        pDirector->runWithScene(pScene);

    Its standard Layer setup logic till now. Fancier things will follow.

3. Enable Touches via Touch Dispatcher

  1. The user should be able to move the Light Saber by dragging. So enable touches in the layer as follows
        // in LightSaber.h
        virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);
        void                            registerWithTouchDispatcher();
    
        // in LightSaber.cpp
        bool LightSaber::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::registerWithTouchDispatcher() {
            CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, INT_MIN, true);
        }
    
        // `init()` of LightSaber
        this->setTouchEnabled(true);
  2. Write a function `createSabers()` that will create a particleSystem, when touch is made.
        void LightSaber::createSabers() {
            CCParticleSystem                *saber;
    
            textureSprite               =   CCSprite::create("swipeParticle.png");
            textureSprite->retain();
    
            saber                      =   CCParticleSun::create();
            saber->setTexture(textureSprite->getTexture());
            saber->setAutoRemoveOnFinish(true);
            saber->setEmissionRate(2000);
            saber->setLife(0.06);
            saber->setLifeVar(0.01);
            saber->setSpeed(0.0);
            saber->setSpeedVar(0.0);
            saber->setStartColor(ccc4f(0.2, 0.2, 0.9, 0.9));
            saber->setEndColor(ccc4f(0.1, 0.1, 0.9, 0.5));
            saber->setStartSize(16.0);
            saber->setStartSizeVar(2.0);
            saber->setEndSize(8.0);
            saber->setEndSizeVar(2.0);
            saber->setBlendAdditive(true);
            saber->setPosition( ccp(2020, 2020) ); // this is to place the saber out of screen initially.
            saberArray->addObject(saber);
            this->addChild(saber);
    
            textureSprite->release();
        }
    
        // in `ccTouchBegan()`
        this->createSabers();
        return true;
  3. Also, write `updateSabers()` and `removeSabers()` functions as follows and call them from `ccTouchMoved()` and `ccTouchEnded()` respectively.
        void LightSaber::updateSabers(CCArray *systems, CCPoint startPoint, CCPoint endPoint) {
            CCParticleSystem                *saber;
            for (int i = 0; i < systems->count(); i++) {
                saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
                saber->setPosition( startPoint );
            }
        }
        void LightSaber::removeSabers() {
            CCObject                        *obj;
            CCParticleSystem                *particleSys;
            CCARRAY_FOREACH(saberArray, obj) {
                particleSys             =   (CCParticleSystem *)obj;
                particleSys->stopSystem();
                particleSys->setAutoRemoveOnFinish(true);
            }
        }

    Now we will have a nice light dot that moves around your finger with a mini trail.

4. Transformation of the `DOT` into a Real Saber (some Math)

Instead of creating a number of particle system along the line of the Saber, I planned to use the `setPosVar()` to spread the particles along the length of the saber. But spreading the available particles across a bigger length reduces the intensity of the whole system. To compensate for this, I create multiple copies of the Particle system and overlays one over other. That is why, we have a `saberArray` instead of a single `CCParticleSystem`.

Light Saber Geomtery

In the above diagram, the touchPoint is P1, which is the holding end of the Saber and the other end of the Saber is P2, which is an offset by certain distance from P1 for now. `ccTouchMoved()` determines this 2 points P1 and P2 and passes it onto `updateSabers()`. Update `updateSabers()` with the following code, which is to determine the spread area and rotation of the saber based on P1 and P2.

    float                           dist;
    CCPoint                         velocity;
    CCPoint                         delta, offset, final;
    CCPoint                         centerPoint;
    CCParticleSystem                *saber;
    float                           ang;

    delta                       =   ccpSub(endPoint, startPoint);
    centerPoint                 =   ccpAdd(ccpMult(delta, 0.5), startPoint);
    ang                         =   atan2f(delta.y, delta.x) * 180 / 3.14;
    dist                        =   ccpDistance(startPoint, endPoint);

    for (int i = 0; i < systems->count(); i++) {
        saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
        saber->setPosition( centerPoint );
        saber->setPosVar( ccp(0, dist * 0.5) );
        saber->setRotation(90 - ang);
    }

In the above code, `delta` represents the distance vector between `startPoint` and `endPoint`, which when multiplied by 0.5, gives half the distance between startPoint and endPoint. This half-distance is then added to the `startPoint` which gives `centerPoint`, where our System will be positioned.

But without proper call to `setPosVar()`, this will again look like a dot offset by some position from the touchPoint. Since the System is now placed exactly in between P1 and P2, whose distance is `dist`, the emissions should be from -dist / 2 to +dist / 2, which is set by the call to `setPosVar()`

Its not yet complete, since the light saber will stand vertically through the centerPoint and it need to be rotated to align to P1 and P2, which is done by the `setRotation()` call.

5. Strengthening the Effect

Since the particle system is stretched along the length of the saber, the saber doesn’t look intensive. So to increase the intensity, lets replicate the particle system and overlay one over the other.

Since we already have an array for the particle system and all our `ccTouchMoved()` and `ccTouchEnded()` functions deal with the array, we need just one more change to achieve this, which is to create more number of Particle Systems and add it to the array, by modifying the `createSabers()` function.

Full Code

void LightSaber::createSabers() {
    CCParticleSystem                *saber;
    textureSprite               =   CCSprite::create("swipeParticle.png");
    textureSprite->retain();
    for (int i = 0; i < strength; i++) {         saber                      =   CCParticleSun::create();         saber->setTexture(textureSprite->getTexture());
        saber->setAutoRemoveOnFinish(true);
        saber->setEmissionRate(2000);
        saber->setLife(0.06);
        saber->setLifeVar(0.01);
        saber->setSpeed(0.0);
        saber->setSpeedVar(0.0);
        saber->setStartColor(ccc4f(0.2, 0.2, 0.9, 0.9));
        saber->setEndColor(ccc4f(0.1, 0.1, 0.9, 0.5));
        saber->setStartSize(16.0);
        saber->setStartSizeVar(2.0);
        saber->setEndSize(8.0);
        saber->setEndSizeVar(2.0);
        saber->setBlendAdditive(true);
        saber->setPosition( ccp(2020, 2020) );
        saberArray->addObject(saber);
        this->addChild(saber);
    }
    textureSprite->release();
}

void LightSaber::updateSabers(CCArray *systems, CCPoint startPoint, CCPoint endPoint) {
    float                           dist;
    CCPoint                         velocity;
    CCPoint                         delta, offset, final;
    CCPoint                         centerPoint;
    CCParticleSystem                *saber;
    float                           ang;
    delta                       =   ccpSub(endPoint, startPoint);
    centerPoint                 =   ccpAdd(ccpMult(delta, 0.5), startPoint);
    ang                         =   atan2f(delta.y, delta.x) * 180 / 3.14;
    dist                        =   ccpDistance(startPoint, endPoint);
    for (int i = 0; i < systems->count(); i++) {
        saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
        saber->setPosition( centerPoint );
        saber->setPosVar( ccp(0, dist * 0.5) );
        saber->setRotation(90 - ang);
    }
}

void LightSaber::removeSabers() {
    CCObject                        *obj;
    CCParticleSystem                *particleSys;
    CCARRAY_FOREACH(saberArray, obj) {
        particleSys             =   (CCParticleSystem *)obj;
        particleSys->stopSystem();
        particleSys->setAutoRemoveOnFinish(true);
    }
}

#pragma mark -
#pragma mark TOUCH OVERRIDES

bool LightSaber::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) {
    this->createSabers();
    return                          true;
}

void LightSaber::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) {
    CCPoint                         point;
    point                       =   pTouch->getLocation();
    this->updateSabers(saberArray, point, ccpAdd(point, ccp(-40, 100)));
}

void LightSaber::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) {
    this->removeSabers();
}

void LightSaber::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) {
}

void LightSaber::registerWithTouchDispatcher() {
    CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, INT_MIN, true);
}

#pragma mark -
#pragma mark OBJECT LIFE CYCLE

bool LightSaber::init() {
    if ( !CCLayer::init() ) {
        return                      false;
    }

    this->setTouchEnabled(true);
    strength                    =   8;
    saberArray                  =   CCArray::create();
    saberArray->retain();
    return                          true;
}

LightSaber::~LightSaber() {
    saberArray->release();
}

Cocos2dx – Spinning Globe by Masking technique

In this post, lets see how we can implement a spinning world in cocos2d-x using masking technique. See the video below for sample output:


Low FPS is because of my Mac Mini; In device, it runs at 60 FPS

Downloads

1. Full Project

1. Create a Cocos2d-x Project

I am not going to discuss about how to setup a cocos2d-x project for iOS here. For info on how to setup cocos2d-x project, refer the official wiki.

All our code will go into `HelloWorld` class that comes with the template.

2. Masking Functionality

The hardest part in achieving our goal would be ‘Masking’ and we resort to Pavel Hancak’s tutorial on cocos2d-x masking. Please check the tutorial on how to mask a sprite. Based on that code, I rewrote the function as below:

CCSprite* HelloWorld::maskedSpriteWithSprite(CCSprite* pTextureSprite, CCSprite* pMaskSprite, float xoffset, float yoffset)
{
    // store the original positions of both sprites
    CCPoint textureSpriteOrigPosition(pTextureSprite->getPosition().x, pTextureSprite->getPosition().y);
    CCPoint maskSpriteOrigPosition(pMaskSprite->getPosition().x, pMaskSprite->getPosition().y);

    // convert the texture sprite position into mask sprite coordinate system
    pTextureSprite->setPosition(ccp(pTextureSprite->getContentSize().width/2 - pMaskSprite->getPosition().x + pMaskSprite->getContentSize().width/2 - xoffset, pTextureSprite->getContentSize().height/2 - pMaskSprite->getPosition().y + pMaskSprite->getContentSize().height/2 + yoffset));

    // position the mask sprite so that the bottom left corner lies on the (o,o) coordinates
    pMaskSprite->setPosition(ccp(pMaskSprite->getContentSize().width/2, pMaskSprite->getContentSize().height/2));

    CCRenderTexture* rt = CCRenderTexture::renderTextureWithWidthAndHeight((int)pMaskSprite->getContentSize().width, (int)pMaskSprite->getContentSize().height);

    ccBlendFunc bfMask = ccBlendFunc();
    bfMask.src = GL_ONE;
    bfMask.dst = GL_ZERO;
    pMaskSprite->setBlendFunc(bfMask);

    // turn off anti-aliasing around the mask sprite
    pMaskSprite->getTexture()->setAliasTexParameters();

    ccBlendFunc bfTexture = ccBlendFunc();
    bfTexture.src = GL_DST_ALPHA;
    bfTexture.dst = GL_ZERO;
    pTextureSprite->setBlendFunc(bfTexture);

    rt->begin();
    pMaskSprite->visit();
    pTextureSprite->visit();
    rt->end();

    // generate the resulting sprite
    CCSprite* pOutcome = CCSprite::spriteWithTexture(rt->getSprite()->getTexture());
    pOutcome->setFlipY(true);

    // restore the original sprite positions
    pTextureSprite->setPosition(textureSpriteOrigPosition);
    pMaskSprite->setPosition(maskSpriteOrigPosition);
    pOutcome->setPosition(maskSpriteOrigPosition);

    return pOutcome;
}

I added two parameters `xoffset` and `yoffset` to the original code. These parameters are to offset the position of underlying map. By varying this offset parameter in gameloop, we move the map linearly over a period of time.

3. Setup Sprites

Add two sprites `map` and `mask`, where

2d world map

`map` contains the full map. Only a part of will be seen at any time

When one edge of the scrolls into the keyhole (mask), we dont want the background to be visible, but the other edge to wrap and fit the gap. For this purpose, we duplicated the map horizontally, and when one edge is within keyhole, the duplicated part will fit in and when the edge crosses the keyhole completely, we reset the map to initial position. (Will see implementation details in subsequent sections).

mask for world map

`mask` picture is like a circular keyhole through which we sees part of a map.

Code to add these two sprites is as follows. Dont add the sprite as child of layer yet.

    
    // in init() function
    map = CCSprite::create("worldmap2d.jpg");
    map->setPosition( ccp(512, 768/2) );
    map->retain();

    mask = CCSprite::create("worldmapmask.png");
    mask->setPosition( ccp(512, 768/2) );
    mask->retain();

Apart from the `map` and `mask`, a star background is added behind.

4. Masking

With Pavel Hancak’s tutorial, masking is now very simple as following:

    // in init() function
    masked = maskedSpriteWithSprite(map, mask, 0, 100);
    addChild(masked);

Only the masked sprite should be added to the `layer`. I found the offsets on trial and error basis 😛 At this point, you should see circular part of the map and not the whole. We masked the map, but still it looks like a flattened map without any depth. To add some depth, add a `shade` picture, above all the sprites as follows

    // in init() function
    shade = CCSprite::create("worldmapshade.png");
    shade->setPosition( ccp(512, 768/2) );
    addChild(shade);
shade over world map

shade to be applied over the world map to create an illusion of depth

5. Spinning Animation

We dont have any real `3D` object here and so we achieve spinning animation by moving the map horizontally behind the keyhole (mask) infinitely. Add this final piece of code and we are done:

    // in init() function
    this->scheduleUpdate();

    void HelloWorld::update(float dt) {
        CCSprite *m;
        xoff += dt * 100;
        if (xoff > 1024) {
            xoff = 0;
        }
        m = maskedSpriteWithSprite(map, mask, xoff, 120);
        masked->setTexture(m->getTexture());
    }

In above code, `xoff` determines the amount of distance that the map should be moved. `Update` is the gamploop, where we increment `xoff` by small amounts in each frame, thus moving the map. When the `xoff` reaches its limit, we reset it. Changing `xoff` doen’t do anything magically. We create a new sprite based on offset position and apply the texture to our `masked` sprite.

Run the project and see the world spinning. Thats the end of the tutorial. Hope it helped :-)

Source can be browsed @ https://github.com/saiy2k/cocos2dx-masking 

Extended EPGLTransitionView

Get the project @ https://github.com/saiy2k/EPGLTransitionView

I was searching for some techniques to do flip screen transitions as in Flipboard iphone app and I found this awesome project, EPGLTransitionView.

EPGLTransitionView is an excellent project to do some cool UIViewController transitions. It basically works by taking screenshots of the two views you would like to switch and manipulating the screenshots with OpenGL commands for cool transition effects. Official URL for the project is http://www.memention.com/blog/2010/02/28/Bells-and-Whistles.html

The project comes with 3 basic transitions:

fall turn flip

Reverse Animation:

The class Demo2Transition in the ‘Demo Project’ provided is exactly what I wanted. But it lacked certain features like turning in reversed direction and turning from bottom up.

So I downloaded the project from github, tinkered with the Demo2Transition class and in a while found a way to reverse the animation. Actually reversing was not a big deal, the animation is controlled by a control variable f, which determines the turned position of view at any given time. I just reversed the initial and final values and it worked flawlessly.

Up Down Transition:

I am not much into OpenGL before, so pulling this’up down turn transition’ took me some time with lots of trial and errors. I did this by changing the order of vertices and texture co-ordinates in which the image is read.

Now both of reversal and up down transition and working great except for 1 frame flicker in ‘reverse animation’. Working on fixing it.

My fork:

I forked the project and committed my changes to the forked repo @ https://github.com/saiy2k/EPGLTransitionView

I also sent a pull request to the original repo, lets see if its accepted. I done a few open source projects before, but this is the first time that I contribute to some one else’s project.

Usage:

I changed the ‘init’ method in the original project into the following:

– (id)initWithView1:(UIView*)view1 andView2:(UIView*)view2 delegate:(id)_delegate fwdDirection:(BOOL)fwd

where `view1` is the UIView which is currently being active and `view2` is the UIView which need to be shown, `delegate` is the transition class that conforms to `EPGLTransitionViewDelegate` protocol, and `fwd` is a flag that determines the direction of transition.

The EPGL TransitionView can reverse the transition only if the Transition class supports it by implementing the `isVertical` property. If it isn’t then, the value of `fwd` doesn’t have any effect.

A sample use case is given below:

UIViewController *controller = [[[UIViewController alloc] init] autorelease];
controller.delegate = self;
controller.view.frame = CGRectMake(0, 20, 320, 460);
NSObject<EPGLTransitionViewDelegate> *transition;
EPGLTransitionView *glview;

transition = [[[Demo2Transition alloc] init] autorelease];
[transition setIsVertical:YES];
glview = [[EPGLTransitionView alloc]
initWithView1:self.view
andView2:controller.view
delegate:transition
fwdDirection:NO];

[glview startTransition];
[self presentModalViewController:controller animated:NO];

Happy flipping.