Source: hitTestContainer.js

import { Container, Point } from "pixi.js";
function defineProperty(name, defaultValue, getter = function (value) { return value; }, setter = function (value) { return value; }) {
    let value = defaultValue;
    let o = {};
    o[name] = {
        get() {
            return getter(value);
        },
        set(para) {
            value = setter(para);
        }
    };
    Object.defineProperties(this, o);
}
/**
 * @class HitContainer
 * @extends PIXI.Container
 * @classdesc Hit test container
 * @property {Number}  sampleCount - 采样个数,如360代表每隔一度采样一次 - 默认:360
 * @property {Number}  sampleMinAlpha - 采样像素点阈值,采样的数据为RGBA中的A 范围在0-255 - 默认:0
 * @property {Number}  sampleDist - 采样衰减距离 - 默认:1像素
 * @example
 * var local = new HitContainer();
 * local.sampleCount = 720;
 * local.sampleMinAlpha = 10;
 * var localSprite = new PIXI.Sprite(res["image_example1"].texture);
 * localSprite.anchor.set(0.5);
 * local.addChild(localSprite);
 * var target = new HitContainer();
 * var targetSprite = new PIXI.Sprite(res["image_example2"].texture);
 * targetSprite.anchor.set(0.5);
 * target.addChild(targetSprite);
 * local.initBound();
 * target.initBound();
 * local.position.set(10,10);
 * console.log(local.hitTest(target));
 * */
class HitContainer extends Container {
    constructor() {
        super();
        defineProperty.call(this, "sampleCount", 360, undefined, function (value) {
            return parseInt(value);
        });
        defineProperty.call(this, "sampleMinAlpha", 0);
        defineProperty.call(this, "sampleDist", 1);
    }
    _toRad(deg) {
        return (deg * Math.PI) / 180;
    }
    /**
     * @function _polarToPixel
     * @memberOf HitContainer
     * @description Caculate the position in Cartesian coordinate with given point in polar coordinate.
     * @param {Number} amplitude Amplitude of polar coordinate point.
     * @param {Number} phase Phase of polar coordinate point.
     * @return {Point} Point in Cartesian coordinate 
     */
    _polarToPixel(amplitude, phase) {
        return {
            x: amplitude * Math.cos(phase),
            y: amplitude * Math.sin(phase)
        };
    }
    /**
     * @function _caculateBoundMatrix
     * @memberOf HitContainer
     * @description Caculate the matrix base on visible pixel in this container.
     * @return {Array} Two-dimensional array contains binnary numbers. 1 means visible and 0 means transparent.  
     */
    _caculateBoundMatrix() {
        var buffer = app.renderer.extract.pixels(this);
        let width = Math.floor(this.width);
        let count = width;
        let arr = [];
        for (let i = 0; i < buffer.length; i += 4) {
            if (count === width) {
                arr.push([]);
                count = 0;
            }
            let alpha = buffer[i + 3];
            arr[arr.length - 1].push(alpha > this.sampleMinAlpha ? 1 : 0);
            count++;
        }
        return arr;
    }
    /**
     * @function _caculateBound
     * @memberOf HitContainer
     * @description Caculate bound area for this in polor coordinate.
     * @param {Number} Two-dimensional array contains binnary numbers.
     * @return {Array} An array contains polor coordinate points.  
     */
    _caculateBound(buffer) {
        let bound = this.getBounds();
        let globalP = this.getGlobalPosition();
        let dx = bound.x - globalP.x;
        let dy = bound.y - globalP.y;
        let sampleAmplitudeMax = Math.floor(
            Math.sqrt(this.width * this.width + this.height * this.height, 2)/2
        );
        let arr = [];
        let me = this;
        let step = 360 / this.sampleCount;
        for (let j = 0; j < 360; j += step) {
            for (let i = sampleAmplitudeMax; i > 0; i -= me.sampleDist) {
                let p = me._polarToPixel(i, me._toRad(j));
                p.x -= dx;
                p.y -= dy;
                if (p.x > me.width || p.y > me.height || p.x < 0 || p.y < 0) {
                    continue;
                }
                let indexX = Math.floor(p.x);
                let indexY = Math.floor(p.y);
                if (buffer[indexY] === undefined || buffer[indexY][indexX] === undefined || buffer[indexY][indexX] === 0) {
                    continue;
                } else {
                    arr.push({
                        amplitude: i,
                        phase: j
                    });
                    break;
                }
            }
        }
        return arr;
    }
    /**
     * @function _getBoundaryGraphics
     * @memberOf HitContainer
     * @description Draw a polygon based on given points in polor coordinate.
     * @para {Array} points in polor coordinate.
     * @return {PIXI.Graphics} Polygon graphic.  
     */
    _getBoundaryGraphics(bound) {
        let gra = new PIXI.Graphics();
        gra.lineStyle(1, 0xff0000, 0);
        gra.beginFill(0xff00ff, 0);
        bound.forEach((element, index) => {
            let relP = this._polarToPixel(element.amplitude, this._toRad(element.phase));
            if (index === 0) {
                gra.moveTo(relP.x, relP.y);
            } else {
                gra.lineTo(relP.x, relP.y);
            }
        });
        gra.endFill();
        return gra;
    }
    /**
     * @function initBound
     * @memberOf HitContainer
     * @description Prepare for hit test. This method must be done before hit test. If the boundary of container is changed. You must do it again.
     * @description 预处理碰撞检测,这个方法必须在碰撞检测前调用。如果容器的边界发生了变化,需要再次调用 
     */
    initBound(boundMatrix,bound) {
        this._BoundMatrix = boundMatrix||this._caculateBoundMatrix();
        this._Bound = bound||this._caculateBound(this._BoundMatrix);
        this._BoundGraphics && this._BoundGraphics.destroy();
        this._BoundGraphics = this._getBoundaryGraphics(this._Bound);
        this.addChild(this._BoundGraphics);
    }
    /**
     * @function isInBound
     * @memberOf HitContainer
     * @para {Object}A point.
     * @description Determine whether a point which defined in global is in the boundary.
     * @description 检测一个全局点是否在边界内
     */
    isInBound(point) {
        return this._BoundGraphics.containsPoint(point);
    }
    /**
     * @function hitTest
     * @memberOf HitContainer
     * @para {HitContainer}Target object.
     * @description Detect whether the container collides with the target area
     * @description 检测容器是否碰撞了目标区域
     * @return {Boolean} Collision with target area or not
     */
    hitTest(target) {
        let isHit = false;
        let p1 = this.getGlobalPosition();
        let targetTestFun = target.isInBound || target.containsPoint;
        this._Bound.some((element, index) => {
            let relP = this._polarToPixel(element.amplitude, (element.phase * Math.PI) / 180);
            relP.x += p1.x;
            relP.y += p1.y;
            isHit = targetTestFun.call(target, relP);
            if (isHit) {
                return true;
            }
        });
        if(!isHit){
            p1 = target.getGlobalPosition();
            target._Bound.some((element, index) => {
                let relP = target._polarToPixel(element.amplitude, (element.phase * Math.PI) / 180);
                relP.x += p1.x;
                relP.y += p1.y;
                isHit = targetTestFun.call(this, relP);
                if (isHit) {
                    return true;
                }
            });
        }
        return isHit;
    }
    /**
     * @function testHitArea
     * @memberOf HitContainer
     * @para {HitContainer}Target object.
     * @description Detect the proportion of hit area.
     * @description 检测碰撞面积比例
     * @return {Number}The proportion of hit area.
     */
    testHitArea(target) {
        if (!this.hitTest(target)) {
            return 0;
        }
        let targetBound = target.getBounds();
        let localBound = this.getBounds();
        let maxEmptyMatrix = this._generateMaxEmptyMatrix(targetBound, localBound);
        maxEmptyMatrix = this._addBoundToMatrix(maxEmptyMatrix, localBound);
        let maxEmptyMatrixTarget = this._generateMaxEmptyMatrix(targetBound, localBound);
        maxEmptyMatrixTarget = target._addBoundToMatrix(maxEmptyMatrixTarget, targetBound);
        let hitMatrix = this._multiplyMatrix(maxEmptyMatrix, maxEmptyMatrixTarget);
        let totalSurfaceArea = this._caculateSuerfaceArea(hitMatrix);
        let localSurfaceArea = this._caculateSuerfaceArea(this._BoundMatrix);
        return totalSurfaceArea / localSurfaceArea;
    }
    /**
     * @function _caculateSuerfaceArea
     * @memberOf HitContainer
     * @para {Array}A matrix which want to be measured.
     * @description Caculate the area based on pixel.
     * @return {Number} Area.
     */
    _caculateSuerfaceArea(matrix) {
        let s = 0;
        for (let i = 0; i < matrix.length; i++) {
            for (let j = 0; j < matrix[i].length; j++) {
                if (matrix[i][j] !== 0 && matrix[i][j] !== undefined) {
                    s++;
                }
            }
        }
        return s;
    }
    /**
     * @function _multiplyMatrix
     * @memberOf HitContainer
     * @para {Array} Matrix 1.
     * @para {Array} Matrix 2.
     * @description Matrix 1 and Matrix 2 must have same shape. The method will multiply every item in matrix 1 and 2. This is not Matrix multiplication.
     * @return {Array} A new matrix which have a same shape with given matrix contains resault.
     */
    _multiplyMatrix(Matrix1, Matrix2) {
        let newMatrix = [];
        for (let i = 0; i < Matrix1.length; i++) {
            newMatrix.push([]);
            for (let j = 0; j < Matrix1[i].length; j++) {
                newMatrix[i][j] = Matrix1[i][j] * Matrix2[i][j];
            }
        }
        return newMatrix;
    }
    /**
     * @function _addBoundToMatrix
     * @memberOf HitContainer
     * @para {Array} Matrix 1.
     * @para {PIXI.Rectangle} localBound.
     * @description Add local pixel in to a Matrix.
     * @return {Array} The matrix after add the local pixel.
     */
    _addBoundToMatrix(emptyMatrix, localBound) {
        let dx = Math.floor(localBound.x - emptyMatrix.bound.x);
        let dy = Math.floor(localBound.y - emptyMatrix.bound.y);
        let localX = 0;
        let localY = 0;
        for (let i = dy; i < emptyMatrix.bound.height; i++) {
            for (let j = dx; j < emptyMatrix.bound.width; j++) {
                if (this._BoundMatrix[localY] === undefined || this._BoundMatrix[localY][localX] === undefined) {
                    continue;
                }
                emptyMatrix[i][j] += this._BoundMatrix[localY][localX];
                localX++;
            }
            localX = 0;
            localY++;
        }
        return emptyMatrix;
    }
    /**
     * @function _generateMaxEmptyMatrix
     * @memberOf HitContainer
     * @para {Array} Matrix 1.
     * @para {Array} Matrix 2.
     * @description Caculate the smallest matrix which can contains both given matrix.
     * @return {Array} An empty matrix.
     */
    _generateMaxEmptyMatrix(bound1, bound2) {
        let x = Math.min(bound1.x, bound2.x);
        let y = Math.min(bound1.y, bound2.y);
        let MaxX = Math.max(bound1.x + bound1.width, bound2.x + bound2.width);
        let MaxY = Math.max(bound1.y + bound1.height, bound2.y + bound2.height);
        let totalWidth = Math.floor(MaxX - x);
        let totalHeight = Math.floor(MaxY - y);
        let totalMatrix = [];
        for (let i = 0; i < totalHeight; i++) {
            let aRow = [];
            for (let j = 0; j < totalWidth; j++) {
                aRow.push(0);
            }
            totalMatrix.push(aRow);
        }
        let maxBound = new PIXI.Rectangle(x, y, totalWidth, totalHeight);
        totalMatrix.bound = maxBound;
        return totalMatrix;
    }
}
export default HitContainer;