Source: src/gameobjects/Rope.js

/**
* @author       Richard Davey <rich@photonstorm.com>
* @author       Mat Groves (@Doormat23)
* @author       Rovanion Luckey
* @copyright    2016 Photon Storm Ltd, Richard Davey
* @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/

/**
* A Rope is a Sprite that has a repeating texture.
*
* The texture will automatically wrap on the edges as it moves.
*
* Please note that Ropes cannot have an input handler.
*
* @class Phaser.Rope
* @constructor
* @extends PIXI.DisplayObjectContainer
* @extends Phaser.Component.Core
* @extends Phaser.Component.Angle
* @extends Phaser.Component.Animation
* @extends Phaser.Component.AutoCull
* @extends Phaser.Component.Bounds
* @extends Phaser.Component.BringToTop
* @extends Phaser.Component.Crop
* @extends Phaser.Component.Delta
* @extends Phaser.Component.Destroy
* @extends Phaser.Component.FixedToCamera
* @extends Phaser.Component.InWorld
* @extends Phaser.Component.LifeSpan
* @extends Phaser.Component.LoadTexture
* @extends Phaser.Component.Overlap
* @extends Phaser.Component.PhysicsBody
* @extends Phaser.Component.Reset
* @extends Phaser.Component.ScaleMinMax
* @extends Phaser.Component.Smoothed
* @param {Phaser.Game} game - A reference to the currently running game.
* @param {number} x - The x coordinate (in world space) to position the Rope at.
* @param {number} y - The y coordinate (in world space) to position the Rope at.
* @param {string|Phaser.RenderTexture|Phaser.BitmapData|PIXI.Texture} key - This is the image or texture used by the Rope during rendering. It can be a string which is a reference to the Cache entry, or an instance of a RenderTexture or PIXI.Texture.
* @param {string|number} frame - If this Rope is using part of a sprite sheet or texture atlas you can specify the exact frame to use by giving a string or numeric index.
* @param {Array} points - An array of {Phaser.Point}.
*/
Phaser.Rope = function (game, x, y, key, frame, points) {

    this.points = [];
    this.points = points;
    this._hasUpdateAnimation = false;
    this._updateAnimationCallback = null;
    x = x || 0;
    y = y || 0;
    key = key || null;
    frame = frame || null;

    /**
    * @property {number} type - The const type of this object.
    * @readonly
    */
    this.type = Phaser.ROPE;

    this.points = points;

    PIXI.DisplayObjectContainer.call(this);

    this.texture = Phaser.Cache.DEFAULT;

    // set up the main bits..
    this.uvs = new Float32Array([0, 1,                                      1, 1,
                                      1, 0,
                                      0, 1]);

    this.vertices = new Float32Array([0, 0,                                            100, 0,
                                            100, 100,
                                            0, 100]);

    this.colors = new Float32Array([1, 1, 1, 1]);

    this.indices = new Uint16Array([0, 1, 2, 3]);

    if (points)
    {
        this.vertices = new Float32Array(points.length * 4);
        this.uvs = new Float32Array(points.length * 4);
        this.colors = new Float32Array(points.length * 2);
        this.indices = new Uint16Array(points.length * 2);
    }

    /**
     * Whether the strip is dirty or not
     *
     * @property dirty
     * @type Boolean
     */
    this.dirty = true;

    /**
     * Triangles in canvas mode are automatically antialiased, use this value to force triangles to overlap a bit with each other.
     *
     * @property canvasPadding
     * @type Number
     */
    this.canvasPadding = 0;

    this.drawMode = Phaser.Rope.TRIANGLE_STRIP;

    Phaser.Component.Core.init.call(this, game, x, y, key, frame);

    this.refresh();

};

Phaser.Rope.prototype = Object.create(PIXI.DisplayObjectContainer.prototype);
Phaser.Rope.prototype.constructor = Phaser.Rope;

Phaser.Component.Core.install.call(Phaser.Rope.prototype, [    'Angle',
    'Animation',
    'AutoCull',
    'Bounds',
    'BringToTop',
    'Crop',
    'Delta',
    'Destroy',
    'FixedToCamera',
    'InWorld',
    'LifeSpan',
    'LoadTexture',
    'Overlap',
    'PhysicsBody',
    'Reset',
    'ScaleMinMax',
    'Smoothed'
]);

Phaser.Rope.prototype.preUpdatePhysics = Phaser.Component.PhysicsBody.preUpdate;
Phaser.Rope.prototype.preUpdateLifeSpan = Phaser.Component.LifeSpan.preUpdate;
Phaser.Rope.prototype.preUpdateInWorld = Phaser.Component.InWorld.preUpdate;
Phaser.Rope.prototype.preUpdateCore = Phaser.Component.Core.preUpdate;

Phaser.Rope.TRIANGLE_STRIP = 0;
Phaser.Rope.TRIANGLES = 1;

/**
* Automatically called by World.preUpdate.
*
* @method Phaser.Rope#preUpdate
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype.preUpdate = function () {

    if (!this.preUpdatePhysics() || !this.preUpdateLifeSpan() || !this.preUpdateInWorld())
    {
        return false;
    }

    return this.preUpdateCore();

};

/**
* Override and use this function in your own custom objects to handle any update requirements you may have.
*
* @method Phaser.Rope#update
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype.update = function() {

    if (this._hasUpdateAnimation)
    {
        this.updateAnimation.call(this);
    }

};

/**
* Resets the Rope. This places the Rope at the given x/y world coordinates and then
* sets alive, exists, visible and renderable all to true. Also resets the outOfBounds state.
* If the Rope has a physics body that too is reset.
*
* @method Phaser.Rope#reset
* @memberof Phaser.Rope
* @param {number} x - The x coordinate (in world space) to position the Sprite at.
* @param {number} y - The y coordinate (in world space) to position the Sprite at.
* @return {Phaser.Rope} This instance.
*/
Phaser.Rope.prototype.reset = function (x, y) {

    Phaser.Component.Reset.prototype.reset.call(this, x, y);

    return this;

};

/*
* Refreshes the rope texture and UV coordinates.
*
* @method Phaser.Rope#refresh
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype.refresh = function () {

    var points = this.points;

    if (points.length < 1)    {
        return;
    }

    var uvs = this.uvs;

    var indices = this.indices;
    var colors = this.colors;

    this.count -= 0.2;

    uvs[0] = 0;
    uvs[1] = 0;
    uvs[2] = 0;
    uvs[3] = 1;

    colors[0] = 1;
    colors[1] = 1;

    indices[0] = 0;
    indices[1] = 1;

    var total = points.length;
    var index;
    var amount;

    for (var i = 1; i < total; i++)    {
        index = i * 4;

        // time to do some smart drawing!
        amount = i / (total - 1);

        if (i % 2)
        {
            uvs[index] = amount;
            uvs[index + 1] = 0;

            uvs[index + 2] = amount;
            uvs[index + 3] = 1;
        }
        else
        {
            uvs[index] = amount;
            uvs[index + 1] = 0;

            uvs[index + 2] = amount;
            uvs[index + 3] = 1;
        }

        index = i * 2;
        colors[index] = 1;
        colors[index + 1] = 1;

        index = i * 2;
        indices[index] = index;
        indices[index + 1] = index + 1;
    }

};

/*
* Updates the Ropes transform ready for rendering.
*
* @method Phaser.Rope#updateTransform
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype.updateTransform = function () {

    var points = this.points;

    if (points.length < 1)    {
        return;
    }

    var lastPoint = points[0];
    var nextPoint;
    var perp = { x:0, y:0 };

    this.count -= 0.2;

    var vertices = this.vertices;
    var total = points.length;
    var point;
    var index;
    var ratio;
    var perpLength;
    var num;

    for (var i = 0; i < total; i++)    {
        point = points[i];
        index = i * 4;

        if(i < points.length - 1)        {
            nextPoint = points[i + 1];
        }
        else
        {
            nextPoint = point;
        }

        perp.y = -(nextPoint.x - lastPoint.x);
        perp.x = nextPoint.y - lastPoint.y;

        ratio = (1 - (i / (total - 1))) * 10;

        if (ratio > 1)
        {
            ratio = 1;
        }

        perpLength = Math.sqrt((perp.x * perp.x) + (perp.y * perp.y));
        num = this.texture.height / 2;
        perp.x /= perpLength;
        perp.y /= perpLength;

        perp.x *= num;
        perp.y *= num;

        vertices[index] = point.x + perp.x;
        vertices[index + 1] = point.y + perp.y;
        vertices[index + 2] = point.x - perp.x;
        vertices[index + 3] = point.y - perp.y;

        lastPoint = point;
    }

    PIXI.DisplayObjectContainer.prototype.updateTransform.call(this);

};

/*
* Sets the Texture this Rope uses for rendering.
*
* @method Phaser.Rope#setTexture
* @memberof Phaser.Rope
* @param {Texture} texture - The texture that will be used.
*/
Phaser.Rope.prototype.setTexture = function (texture) {

    this.texture = texture;

};

/*
* Renders the Rope to WebGL.
*
* @private
* @method Phaser.Rope#_renderWebGL
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderWebGL = function (renderSession) {

    if (!this.visible || this.alpha <= 0)    {
        return;
    }

    renderSession.spriteBatch.stop();

    if (!this._vertexBuffer)
    {
        this._initWebGL(renderSession);
    }

    renderSession.shaderManager.setShader(renderSession.shaderManager.stripShader);

    this._renderStrip(renderSession);

    renderSession.spriteBatch.start();

};

/*
* Builds the Strip.
*
* @private
* @method Phaser.Rope#_initWebGL
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._initWebGL = function (renderSession) {

    // build the strip!
    var gl = renderSession.gl;

    this._vertexBuffer = gl.createBuffer();
    this._indexBuffer = gl.createBuffer();
    this._uvBuffer = gl.createBuffer();
    this._colorBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.DYNAMIC_DRAW);

    gl.bindBuffer(gl.ARRAY_BUFFER, this._uvBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.uvs, gl.STATIC_DRAW);

    gl.bindBuffer(gl.ARRAY_BUFFER, this._colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.colors, gl.STATIC_DRAW);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW);

};

/*
* Renders the Strip to WebGL.
*
* @private
* @method Phaser.Rope#_renderStrip
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderStrip = function (renderSession) {

    var gl = renderSession.gl;
    var projection = renderSession.projection;
    var offset = renderSession.offset;
    var shader = renderSession.shaderManager.stripShader;

    var drawMode = (this.drawMode === Phaser.Rope.TRIANGLE_STRIP) ? gl.TRIANGLE_STRIP : gl.TRIANGLES;

    renderSession.blendModeManager.setBlendMode(this.blendMode);

    // set uniforms
    gl.uniformMatrix3fv(shader.translationMatrix, false, this.worldTransform.toArray(true));
    gl.uniform2f(shader.projectionVector, projection.x, -projection.y);
    gl.uniform2f(shader.offsetVector, -offset.x, -offset.y);
    gl.uniform1f(shader.alpha, this.worldAlpha);

    if (!this.dirty)
    {
        gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
        gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices);
        gl.vertexAttribPointer(shader.aVertexPosition, 2, gl.FLOAT, false, 0, 0);

        // update the uvs
        gl.bindBuffer(gl.ARRAY_BUFFER, this._uvBuffer);
        gl.vertexAttribPointer(shader.aTextureCoord, 2, gl.FLOAT, false, 0, 0);

        gl.activeTexture(gl.TEXTURE0);

        // check if a texture is dirty..
        if (this.texture.baseTexture._dirty[gl.id])
        {
            renderSession.renderer.updateTexture(this.texture.baseTexture);
        }
        else
        {
            // bind the current texture
            gl.bindTexture(gl.TEXTURE_2D, this.texture.baseTexture._glTextures[gl.id]);
        }

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
    }
    else
    {
        this.dirty = false;

        gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, this.vertices, gl.STATIC_DRAW);
        gl.vertexAttribPointer(shader.aVertexPosition, 2, gl.FLOAT, false, 0, 0);

        // update the uvs
        gl.bindBuffer(gl.ARRAY_BUFFER, this._uvBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, this.uvs, gl.STATIC_DRAW);
        gl.vertexAttribPointer(shader.aTextureCoord, 2, gl.FLOAT, false, 0, 0);

        gl.activeTexture(gl.TEXTURE0);

        // check if a texture is dirty..
        if (this.texture.baseTexture._dirty[gl.id])
        {
            renderSession.renderer.updateTexture(this.texture.baseTexture);
        }
        else
        {
            gl.bindTexture(gl.TEXTURE_2D, this.texture.baseTexture._glTextures[gl.id]);
        }

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW);
    }

    gl.drawElements(drawMode, this.indices.length, gl.UNSIGNED_SHORT, 0);

};

/*
* Renders the Strip to Canvas.
*
* @private
* @method Phaser.Rope#_renderCanvas
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderCanvas = function (renderSession) {

    var context = renderSession.context;

    var transform = this.worldTransform;

    var tx = (transform.tx * renderSession.resolution) + renderSession.shakeX;
    var ty = (transform.ty * renderSession.resolution) + renderSession.shakeY;

    if (renderSession.roundPixels)
    {
        context.setTransform(transform.a, transform.b, transform.c, transform.d, tx | 0, ty | 0);
    }
    else
    {
        context.setTransform(transform.a, transform.b, transform.c, transform.d, tx, ty);
    }

    if (this.drawMode === Phaser.Rope.TRIANGLE_STRIP)
    {
        this._renderCanvasTriangleStrip(context);
    }
    else
    {
        this._renderCanvasTriangles(context);
    }

};

/*
* Renders a Triangle Strip to Canvas.
*
* @private
* @method Phaser.Rope#_renderCanvasTriangleStrip
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderCanvasTriangleStrip = function (context) {

    // draw triangles!!
    var vertices = this.vertices;
    var uvs = this.uvs;

    var length = vertices.length / 2;

    this.count++;

    for (var i = 0; i < length - 2; i++)    {
        var index = i * 2;
        this._renderCanvasDrawTriangle(context, vertices, uvs, index, (index + 2), (index + 4));
    }

};

/*
* Renders a Triangle to Canvas.
*
* @private
* @method Phaser.Rope#_renderCanvasTriangles
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderCanvasTriangles = function (context) {

    var vertices = this.vertices;
    var uvs = this.uvs;
    var indices = this.indices;

    var length = indices.length;

    this.count++;

    for (var i = 0; i < length; i += 3)    {
        var index0 = indices[i] * 2;
        var index1 = indices[i + 1] * 2;
        var index2 = indices[i + 2] * 2;

        this._renderCanvasDrawTriangle(context, vertices, uvs, index0, index1, index2);
    }

};

/*
* Renders a Triangle to Canvas.
*
* @private
* @method Phaser.Rope#_renderCanvasDrawTriangle
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype._renderCanvasDrawTriangle = function (context, vertices, uvs, index0, index1, index2) {

    var textureSource = this.texture.baseTexture.source;
    var textureWidth = this.texture.width;
    var textureHeight = this.texture.height;

    var x0 = vertices[index0];
    var x1 = vertices[index1];
    var x2 = vertices[index2];
    var y0 = vertices[index0 + 1];
    var y1 = vertices[index1 + 1];
    var y2 = vertices[index2 + 1];

    var u0 = uvs[index0] * textureWidth;
    var u1 = uvs[index1] * textureWidth;
    var u2 = uvs[index2] * textureWidth;
    var v0 = uvs[index0 + 1] * textureHeight;
    var v1 = uvs[index1 + 1] * textureHeight;
    var v2 = uvs[index2 + 1] * textureHeight;

    if (this.canvasPadding > 0)
    {
        var paddingX = this.canvasPadding / this.worldTransform.a;
        var paddingY = this.canvasPadding / this.worldTransform.d;
        var centerX = (x0 + x1 + x2) / 3;
        var centerY = (y0 + y1 + y2) / 3;

        var normX = x0 - centerX;
        var normY = y0 - centerY;

        var dist = Math.sqrt((normX * normX) + (normY * normY));
        x0 = centerX + (normX / dist) * (dist + paddingX);
        y0 = centerY + (normY / dist) * (dist + paddingY);

        normX = x1 - centerX;
        normY = y1 - centerY;

        dist = Math.sqrt((normX * normX) + (normY * normY));
        x1 = centerX + (normX / dist) * (dist + paddingX);
        y1 = centerY + (normY / dist) * (dist + paddingY);

        normX = x2 - centerX;
        normY = y2 - centerY;

        dist = Math.sqrt((normX * normX) + (normY * normY));
        x2 = centerX + (normX / dist) * (dist + paddingX);
        y2 = centerY + (normY / dist) * (dist + paddingY);
    }

    context.save();
    context.beginPath();

    context.moveTo(x0, y0);
    context.lineTo(x1, y1);
    context.lineTo(x2, y2);

    context.closePath();

    context.clip();

    // Compute matrix transform
    var delta =  (u0 * v1)      + (v0 * u2)      + (u1 * v2)      - (v1 * u2)      - (v0 * u1)      - (u0 * v2);
    var deltaA = (x0 * v1)      + (v0 * x2)      + (x1 * v2)      - (v1 * x2)      - (v0 * x1)      - (x0 * v2);
    var deltaB = (u0 * x1)      + (x0 * u2)      + (u1 * x2)      - (x1 * u2)      - (x0 * u1)      - (u0 * x2);
    var deltaC = (u0 * v1 * x2) + (v0 * x1 * u2) + (x0 * u1 * v2) - (x0 * v1 * u2) - (v0 * u1 * x2) - (u0 * x1 * v2);
    var deltaD = (y0 * v1)      + (v0 * y2)      + (y1 * v2)      - (v1 * y2)      - (v0 * y1)      - (y0 * v2);
    var deltaE = (u0 * y1)      + (y0 * u2)      + (u1 * y2)      - (y1 * u2)      - (y0 * u1)      - (u0 * y2);
    var deltaF = (u0 * v1 * y2) + (v0 * y1 * u2) + (y0 * u1 * v2) - (y0 * v1 * u2) - (v0 * u1 * y2) - (u0 * y1 * v2);

    context.transform(        deltaA / delta,
        deltaD / delta,
        deltaB / delta,
        deltaE / delta,
        deltaC / delta,
        deltaF / delta);

    context.drawImage(textureSource, 0, 0);
    context.restore();

};

/*
* Renders a flat strip.
*
* @method Phaser.Rope#renderStripFlat
* @memberof Phaser.Rope
*/
Phaser.Rope.prototype.renderStripFlat = function (strip) {

    var context = this.context;
    var vertices = strip.vertices;

    var length = vertices.length / 2;

    this.count++;

    context.beginPath();

    for (var i = 1; i < length - 2; i++)    {
        // draw some triangles!
        var index = i * 2;

        var x0 = vertices[index];
        var x1 = vertices[index + 2];
        var x2 = vertices[index + 4];
        var y0 = vertices[index + 1];
        var y1 = vertices[index + 3];
        var y2 = vertices[index + 5];

        context.moveTo(x0, y0);
        context.lineTo(x1, y1);
        context.lineTo(x2, y2);
    }

    context.fillStyle = '#FF0000';
    context.fill();
    context.closePath();

};

/*
* Returns the bounds of the mesh as a rectangle. The bounds calculation takes the worldTransform into account.
*
* @method Phaser.Rope#getBounds
* @memberof Phaser.Rope
* @param {Matrix} matrix - The transformation matrix of the Sprite.
* @return {Rectangle} The framing rectangle.
*/
Phaser.Rope.prototype.getBounds = function (matrix) {

    var worldTransform = matrix || this.worldTransform;

    var a = worldTransform.a;
    var b = worldTransform.b;
    var c = worldTransform.c;
    var d = worldTransform.d;
    var tx = worldTransform.tx;
    var ty = worldTransform.ty;

    var maxX = -Infinity;
    var maxY = -Infinity;

    var minX = Infinity;
    var minY = Infinity;

    var vertices = this.vertices;

    for (var i = 0; i < vertices.length; i += 2)    {
        var rawX = vertices[i];
        var rawY = vertices[i + 1];
        var x = (a * rawX) + (c * rawY) + tx;
        var y = (d * rawY) + (b * rawX) + ty;

        minX = x < minX ? x : minX;        minY = y < minY ? y : minY;
        maxX = x > maxX ? x : maxX;
        maxY = y > maxY ? y : maxY;
    }

    if (minX === -Infinity || maxY === Infinity)
    {
        return PIXI.EmptyRectangle;
    }

    var bounds = this._bounds;

    bounds.x = minX;
    bounds.width = maxX - minX;

    bounds.y = minY;
    bounds.height = maxY - minY;

    //  Store a reference so that if this function gets called again in the render cycle we do not have to recalculate
    this._currentBounds = bounds;

    return bounds;

};

/**
* A Rope will call its updateAnimation function on each update loop if it has one.
*
* @name Phaser.Rope#updateAnimation
* @property {function} updateAnimation - Set to a function if you'd like the rope to animate during the update phase. Set to false or null to remove it.
*/
Object.defineProperty(Phaser.Rope.prototype, "updateAnimation", {
    get: function () {

        return this._updateAnimation;

    },

    set: function (value) {

        if (value && typeof value === 'function')
        {
            this._hasUpdateAnimation = true;
            this._updateAnimation = value;
        }
        else
        {
            this._hasUpdateAnimation = false;
            this._updateAnimation = null;
        }

    }

});

/**
* The segments that make up the rope body as an array of Phaser.Rectangles
*
* @name Phaser.Rope#segments
* @property {Phaser.Rectangles[]} updateAnimation - Returns an array of Phaser.Rectangles that represent the segments of the given rope
*/
Object.defineProperty(Phaser.Rope.prototype, "segments", {
    get: function () {

        var segments = [];
        var index, x1, y1, x2, y2, width, height, rect;

        for (var i = 0; i < this.points.length; i++)        {
            index = i * 4;

            x1 = this.vertices[index] * this.scale.x;
            y1 = this.vertices[index + 1] * this.scale.y;
            x2 = this.vertices[index + 4] * this.scale.x;
            y2 = this.vertices[index + 3] * this.scale.y;

            width = Phaser.Math.difference(x1, x2);
            height = Phaser.Math.difference(y1, y2);

            x1 += this.world.x;
            y1 += this.world.y;
            rect = new Phaser.Rectangle(x1, y1, width, height);
            segments.push(rect);
        }

        return segments;
    }

});