Source: instance/Instance.js

instance/Instance.js

/**
 * 全てのライブラリの親クラス
 * Parent class for all libraries
 *
 * @class
 * @memberOf instance
 */
class Instance
{
    /**
     * @param {object} object
     * @constructor
     * @public
     */
    constructor (object)
    {
        this._$id       = object.id;
        this._$name     = object.name;
        this._$type     = object.type;
        this._$symbol   = object.symbol || "";
        this._$folderId = object.folderId | 0;
    }

    /**
     * @description 初期起動関数
     *              initial invoking function
     *
     * @return {void}
     * @method
     * @abstract
     */
    // eslint-disable-next-line no-empty-function
    initialize () {}

    /**
     * @description クラス内の変数をObjectにして返す
     *              Return variables in a class as Objects
     *
     * @return {object}
     * @method
     * @abstract
     */
    // eslint-disable-next-line no-empty-function
    toObject () {}

    /**
     * @description 書き出し用のObjectを返す
     *              Returns an Object for export
     *
     * @return {object}
     * @method
     * @abstract
     */
    // eslint-disable-next-line no-empty-function
    toPublish () {}

    /**
     * @description シンボルを指定した時の継承先を返す
     *              Returns the inheritance destination when a symbol is specified.
     *
     * @readonly
     * @abstract
     */
    // eslint-disable-next-line no-empty-function,getter-return
    get defaultSymbol () {}

    /**
     * @description Next2DのDisplayObjectを生成
     *              Generate Next2D DisplayObject
     *
     * @return {*}
     * @method
     * @abstract
     */
    // eslint-disable-next-line no-empty-function
    createInstance () {}

    /**
     * @description 表示領域(バウンディングボックス)のObjectを返す
     *              Returns the Object of the display area (bounding box)
     *
     * @param  {array} [matrix=null]
     * @return {object}
     * @method
     * @abstract
     */
    // eslint-disable-next-line no-empty-function,no-unused-vars
    getBounds (matrix = null) {}

    /**
     * @description ライブラリ内のユニークな値
     *              Unique value in the library
     *
     * @member {number}
     * @public
     */
    get id ()
    {
        return this._$id;
    }
    set id (id)
    {
        this._$id = id | 0;
    }

    /**
     * @description 格納先のフォルダID
     *              Destination folder ID
     *
     * @default 0
     * @member {number}
     * @public
     */
    get folderId ()
    {
        return this._$folderId;
    }
    set folderId (folder_id)
    {
        this._$folderId = folder_id | 0;
    }

    /**
     * @description フォルダを含めたライブラリのパスを返す
     *              Returns the path to the library, including folders
     *
     * @member {string}
     * @readonly
     * @public
     */
    get path ()
    {
        const workSpace = Util.$currentWorkSpace();

        let path = this._$name;
        if (this._$folderId) {
            let parent = this;
            while (parent._$folderId) {
                parent = workSpace.getLibrary(parent._$folderId);
                path = `${parent._$name}/${path}`;
            }
        }

        return path;
    }

    /**
     * @description 指定したワークスペースからPathを取得
     *              Get Path from the specified workspace
     *
     * @param  {WorkSpace} work_space
     * @return {string}
     * @method
     * @public
     */
    getPathWithWorkSpace (work_space)
    {
        let path = this._$name;
        if (this._$folderId) {
            let parent = this;
            while (parent._$folderId) {
                parent = work_space.getLibrary(parent._$folderId);
                path = `${parent._$name}/${path}`;
            }
        }

        return path;
    }

    /**
     * @description ライブラリ名、フォルダのパスを含めた名前をユニークとして利用する
     *              Use the name including the library name and folder path as unique
     *
     * @member {string}
     * @public
     */
    get name ()
    {
        return this._$name;
    }
    set name (name)
    {
        this._$name = `${name}`;

        if (this.id) {
            // ライブラリのelementのテキストも更新
            const element = document
                .getElementById(`library-name-${this.id}`);
            if (element) {
                element.textContent = `${this._$name}`;
            }
        }

        // コントローラーに表示中のシーンの場合は、コントローラー表示も更新
        const workSpace = Util.$currentWorkSpace();
        if (workSpace && workSpace.scene.id === this.id) {

            const objectName = document.getElementById("object-name");
            if (objectName) {
                objectName.value = `${this._$name}`;
            }

            const sceneName = document.getElementById("scene-name");
            if (sceneName) {
                sceneName.textContent = `${this._$name}`;
            }

        }

    }

    /**
     * @description ライブラリの方の値、InstanceTypeクラスの固定値を参照
     *              See the value toward the library, fixed value in the InstanceType class.
     *
     * @member {string}
     * @public
     */
    get type ()
    {
        return this._$type;
    }
    set type (type)
    {
        type = `${type}`.toLowerCase();
        switch (type) {

            case InstanceType.SHAPE:
            case InstanceType.BITMAP:
            case InstanceType.VIDEO:
            case InstanceType.FOLDER:
            case InstanceType.SOUND:
            case InstanceType.MOVIE_CLIP:
            case InstanceType.TEXT:
                this._$type = type;
                break;

            default:
                break;

        }
    }

    /**
     * @description Next2D Playerでのシンボルアクセス用の値
     *              Value for symbol access in Next2D Player
     *
     * @member {string}
     * @public
     */
    get symbol ()
    {
        return this._$symbol;
    }
    set symbol (symbol)
    {
        this._$symbol = `${symbol}`;

        if (this.id) {
            // ライブラリ内のelementのテキストデータも更新
            const element = document
                .getElementById(`library-symbol-name-${this.id}`);
            if (element) {
                element.textContent = `${symbol}`;
            }
        }
    }

    /**
     * @description このアイテムが設定されたDisplayObjectが選択された時
     *              内部情報をコントローラーに表示する
     *              When a DisplayObject with this item set is selected,
     *              internal information is displayed on the controller.
     *
     * @param {object} place
     * @param {string} [name=""]
     * @public
     */
    showController (place, name = "")
    {
        Util.$controller.hideObjectSetting([
            "sound-setting",
            "ease-setting"
        ]);

        Util.$controller.showObjectSetting([
            "object-setting",
            "reference-setting",
            "transform-setting",
            "color-setting",
            "blend-setting",
            "filter-setting",
            "instance-setting"
        ]);

        // 選択されたインスタンス名をセット
        Util
            .$instanceSelectController
            .createInstanceSelect(this);

        // 名前とシンボルの値をセット
        document
            .getElementById("object-name")
            .value = name;

        document
            .getElementById("object-symbol")
            .value = this.symbol;

        // matrixの値をセット
        const matrix  = place.matrix;
        const xScale  = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]);
        const yScale  = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]);
        const radianX = Math.atan2( matrix[1], matrix[0]) * Util.$Rad2Deg;
        const radianY = Math.atan2(-matrix[2], matrix[3]) * Util.$Rad2Deg;

        document
            .getElementById("transform-scale-x")
            .value = Math.abs(Math.ceil(radianX - radianY)) >= 180
                ? xScale * -100
                : xScale * 100;

        document
            .getElementById("transform-scale-y")
            .value = yScale * 100;

        document
            .getElementById("transform-rotate")
            .value = radianX;

        // ColorTransformの値をセット
        const colorTransform = place.colorTransform;

        document
            .getElementById("color-red-multiplier")
            .value = colorTransform[0] * 100;
        document
            .getElementById("color-green-multiplier")
            .value = colorTransform[1] * 100;
        document
            .getElementById("color-blue-multiplier")
            .value = colorTransform[2] * 100;
        document
            .getElementById("color-alpha-multiplier")
            .value = colorTransform[3] * 100;

        document
            .getElementById("color-red-offset")
            .value = colorTransform[4];
        document
            .getElementById("color-green-offset")
            .value = colorTransform[5];
        document
            .getElementById("color-blue-offset")
            .value = colorTransform[6];
        document
            .getElementById("color-alpha-offset")
            .value = colorTransform[7];

        // 指定したブレンドモードにselectedを設定
        const children = document
            .getElementById("blend-select")
            .children;

        for (let idx = 0; idx < children.length; ++idx) {

            const node = children[idx];

            if (node.value !== place.blendMode) {
                continue;
            }

            node.selected = true;
            break;
        }

        // フィルター情報を初期化
        Util.$filterController.clearFilters();

        // フィルターがあれば対象のElementを追加
        const filterElement = document
            .getElementById("filter-setting-list");

        const length = place.filter.length;
        if (length) {
            document
                .querySelectorAll(".filter-none")[0]
                .style.display = "none";
        }

        for (let idx = 0; idx < length; ++idx) {
            const filter = place.filter[idx];
            Util
                .$filterController[`add${filter.name}`](
                    filterElement, filter, false
                );
        }
    }

    /**
     * @description Next2DのBitmapDataクラスを経由してImageクラスを生成
     *              Generate Image class via Next2D BitmapData class
     *
     * @param  {number}  width
     * @param  {number}  height
     * @param  {object}  place
     * @param  {object}  [range = null]
     * @param  {number}  [static_frame = 0]
     * @return {HTMLImageElement}
     * @method
     * @public
     */
    toImage (width, height, place, range = null, static_frame = 0)
    {
        // empty image
        if (!width || !height) {
            return new Image();
        }

        const { Matrix } = window.next2d.geom;

        const instance = this.createInstance(place, range, static_frame);

        const object = this.calcFilter(width, height, place);
        instance.filters = object.filters;

        const container  = this.createContainer(instance, place);
        const bitmapData = this.createBitmapData(object.width, object.height);

        const ratio = window.devicePixelRatio * Util.$zoomScale;
        const drawBounds = container.getBounds(container);

        let tx = -drawBounds.x;
        if (0 > object.offsetX) {
            tx -= object.offsetX * ratio;
        }

        let ty = -drawBounds.y;
        if (0 > object.offsetY) {
            ty -= object.offsetY * ratio;
        }

        bitmapData.draw(container, new Matrix(1, 0, 0, 1, tx, ty));

        const image = new Image();
        image.src = bitmapData.toDataURL();
        bitmapData.dispose();

        image.width     = object.width;
        image.height    = object.height;
        image._$width   = object.width;
        image._$height  = object.height;
        image.draggable = false;

        const bounds = this.getBounds(place.matrix, place, range);

        image._$tx = bounds.xMin;
        image._$ty = bounds.yMin;

        image._$offsetX = 0 > object.offsetX ? object.offsetX : 0;
        image._$offsetY = 0 > object.offsetY ? object.offsetY : 0;

        return image;
    }

    /**
     * @description 指定されたFilterの描画範囲を計算
     *              Calculates the drawing range of the specified Filter
     *
     * @param  {number} width
     * @param  {number} height
     * @param  {object} place
     * @return {object}
     * @method
     * @public
     */
    calcFilter (width, height, place)
    {
        const { Rectangle } = window.next2d.geom;

        let xScale = Math.sqrt(
            place.matrix[0] * place.matrix[0]
            + place.matrix[1] * place.matrix[1]
        );

        let yScale = Math.sqrt(
            place.matrix[2] * place.matrix[2]
            + place.matrix[3] * place.matrix[3]
        );

        const object = {
            "width":   width,
            "height":  height,
            "offsetX": 0,
            "offsetY": 0,
            "filters": []
        };

        if (place.filter.length) {

            let rect = new Rectangle(0, 0, width, height);

            for (let idx = 0; idx < place.filter.length; ++idx) {

                const filter = place.filter[idx];
                if (!filter.state) {
                    continue;
                }

                const instance = filter.createInstance();

                rect = instance._$generateFilterRect(rect, xScale, yScale);

                object.filters.push(instance);

            }

            object.width  = Math.ceil(rect.width);
            object.height = Math.ceil(rect.height);

            object.offsetX = rect.x;
            object.offsetY = rect.y;
        }

        return object;
    }

    /**
     * @description BitmapDataに渡すSpriteを生成
     *              Generate Sprite to be passed to BitmapData
     *
     * @param  {DisplayObject} instance
     * @param  {object} place
     * @return {next2d.display.Sprite}
     * @method
     * @public
     */
    createContainer (instance, place)
    {
        const { Sprite } = window.next2d.display;
        const { Matrix, ColorTransform } = window.next2d.geom;

        instance
            .transform
            .matrix = new Matrix(
                place.matrix[0], place.matrix[1],
                place.matrix[2], place.matrix[3],
                place.matrix[4], place.matrix[5]
            );

        // fixed logic
        instance
            .transform
            .colorTransform = new ColorTransform(
                place.colorTransform[0], place.colorTransform[1],
                place.colorTransform[2], place.colorTransform[3],
                place.colorTransform[4], place.colorTransform[5],
                place.colorTransform[6], place.colorTransform[7]
            );

        const sprite = new Sprite();
        sprite.addChild(instance);

        const ratio = window.devicePixelRatio * Util.$zoomScale;
        sprite.scaleX = ratio;
        sprite.scaleY = ratio;

        const container = new Sprite();
        container.addChild(sprite);

        return container;
    }

    /**
     * @description 幅と高さを指定して、BitmapDataクラスを生成
     *              Generate BitmapData class by specifying width and height
     *
     * @param  {number} width
     * @param  {number} height
     * @return {next2d.display.BitmapData}
     * @method
     * @public
     */
    createBitmapData (width, height)
    {
        const { BitmapData } = window.next2d.display;

        const ratio = window.devicePixelRatio * Util.$zoomScale;

        return new BitmapData(
            Math.ceil(width  * ratio),
            Math.ceil(height * ratio),
            true, 0
        );
    }

    /**
     * @description プレビュー用のImageクラスを生成
     *              Generate Image class for preview
     *
     * @return {HTMLImageElement}
     * @method
     * @public
     */
    getPreview ()
    {
        if (this.type === InstanceType.FOLDER) {
            return new Image();
        }

        const bounds = this.getBounds([1, 0, 0, 1, 0, 0]);

        // size
        let width  = Math.abs(bounds.xMax - bounds.xMin);
        let height = Math.abs(bounds.yMax - bounds.yMin);
        if (!width || !height) {
            return new Image();
        }

        let scaleX   = 1;
        const scaleY = 150 / height;

        width  = width  * scaleY | 0;
        height = height * scaleY | 0;

        const controllerWidth = (document
            .documentElement
            .style
            .getPropertyValue("--controller-width")
            .split("px")[0] | 0) - 10;

        if (width > controllerWidth) {
            scaleX = controllerWidth / width;
            width  = width  * scaleX | 0;
            height = height * scaleX | 0;
        }

        const image = this.toImage(
            Math.ceil(width),
            Math.ceil(height),
            {
                "frame": 1,
                "matrix": [scaleY * scaleX, 0, 0, scaleY * scaleX, 0, 0],
                "colorTransform": [1, 1, 1, 1, 0, 0, 0, 0],
                "blendMode": "normal",
                "filter": []
            },
            null, 0, true
        );

        if (image.height !== height) {
            const height = Math.min(150, image.height);
            image.width  *= height / image.height;
            image.height  = height;
        }

        return image;
    }

    /**
     * @description ライブラリからの削除処理、配置先からも削除を行う
     *              Process deletion from the library and also from the placement site.
     *
     * @return {void}
     * @method
     * @public
     */
    remove ()
    {
        const workSpace = Util.$currentWorkSpace();

        const scene = workSpace.scene;
        for (let instance of workSpace._$libraries.values()) {

            if (instance.type !== InstanceType.MOVIE_CLIP) {
                continue;
            }

            // 削除するインスタンスならスキップ
            if (instance.id === this.id) {
                continue;
            }

            for (let layer of instance._$layers.values()) {

                let reload = false;

                // 削除してもいいようにクローンして利用する
                const characters = layer._$characters.slice();

                for (let idx = 0; idx < characters.length; ++idx) {

                    const character = characters[idx];
                    if (this.id === character.libraryId) {

                        for (const keyFrame of character._$places.keys()) {

                            const range = character.getRange(keyFrame);

                            // 空のキーフレームがあればスキップ
                            if (layer.getActiveEmptyCharacter(keyFrame)) {
                                continue;
                            }

                            // DisplayObjectが配置されていればスキップ
                            const activeCharacters = layer.getActiveCharacter(keyFrame);
                            let done = false;
                            for (let idx = 0; idx < activeCharacters.length; ++idx) {

                                const activeCharacter = activeCharacters[idx];
                                if (activeCharacter.id === character.id) {
                                    continue;
                                }

                                done = true;
                            }

                            if (done) {
                                continue;
                            }

                            // 削除するレンジに空のキーフレームを登録
                            layer.addEmptyCharacter(new EmptyCharacter({
                                "startFrame": range.startFrame,
                                "endFrame": range.endFrame
                            }));
                        }

                        // 登録先のレイヤーから削除
                        layer.deleteCharacter(character.id);
                        reload = true;
                    }
                }

                // 表示中のレイヤーならスタイルを更新
                if (reload && scene.id === instance.id) {
                    layer.reloadStyle();
                }
            }

            if (instance._$sounds.size) {
                for (const [frame, sounds] of instance._$sounds) {

                    const pool = [];
                    for (let idx = 0; idx < sounds.length; ++idx) {

                        const sound = sounds[idx];
                        if (this.id === sound.characterId) {
                            continue;
                        }

                        pool.push(sound);
                    }

                    // 削除対象以外を再登録
                    instance._$sounds.set(frame, pool);
                }
            }
        }
    }
}