Source: view/controller/TweenController.js

view/controller/TweenController.js

/**
 * @class
 * @extends {BaseController}
 * @memberOf view.controller
 */
class TweenController extends BaseController
{
    /**
     * @constructor
     * @public
     */
    constructor()
    {
        super("ease");

        /**
         * @type {HTMLDivElement}
         * @default null
         * @private
         */
        this._$easeTarget = null;

        /**
         * @type {CanvasRenderingContext2D}
         * @default null
         * @private
         */
        this._$viewContext = null;

        /**
         * @type {CanvasRenderingContext2D}
         * @default null
         * @private
         */
        this._$drawContext = null;

        /**
         * @type {function}
         * @default null
         * @private
         */
        this._$moveCurvePointer = null;

        /**
         * @type {function}
         * @default null
         * @private
         */
        this._$endMoveCurvePointer = null;

        /**
         * @type {function}
         * @default null
         * @private
         */
        this._$moveEasingPointer = null;

        /**
         * @type {function}
         * @default null
         * @private
         */
        this._$endMoveEasingPointer = null;

        /**
         * @type {function}
         * @default null
         * @private
         */
        this._$deleteEasingPointer = null;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_CANVAS_WIDTH ()
    {
        return 300;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_CANVAS_HEIGHT ()
    {
        return 400;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_BASE_CANVAS_SIZE ()
    {
        return 200;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_MIN_POINTER_X ()
    {
        return 6;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_MIN_POINTER_Y ()
    {
        return -5;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_MAX_POINTER_X ()
    {
        return 306;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_MAX_POINTER_Y ()
    {
        return 395;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_SCREEN_X ()
    {
        return 57;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_SCREEN_Y ()
    {
        return 94;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_MOVE_Y ()
    {
        return 294;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_OFFSET_X ()
    {
        return 50;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_OFFSET_Y ()
    {
        return 100;
    }

    /**
     * @return {number}
     * @const
     * @static
     */
    static get EASE_RANGE ()
    {
        return 100;
    }

    /**
     * @description 初期起動関数
     *
     * @return {void}
     * @method
     * @public
     */
    initialize ()
    {
        super.initialize();

        const ratio = window.devicePixelRatio;

        const drawCanvas   = document.createElement("canvas");
        drawCanvas.width   = TweenController.EASE_CANVAS_WIDTH  * ratio;
        drawCanvas.height  = TweenController.EASE_CANVAS_HEIGHT * ratio;
        this._$drawContext = drawCanvas.getContext("2d");

        const viewCanvas = document.getElementById("ease-custom-canvas");
        if (viewCanvas) {

            viewCanvas.width  = TweenController.EASE_CANVAS_WIDTH  * ratio;
            viewCanvas.height = TweenController.EASE_CANVAS_HEIGHT * ratio;

            viewCanvas.style.transform          = `scale(${1 / ratio}, ${1 / ratio})`;
            viewCanvas.style.backfaceVisibility = "hidden";

            this._$viewContext = viewCanvas.getContext("2d");

            // 新規カーブポインター追加処理
            viewCanvas.addEventListener("dblclick", (event) =>
            {
                this.addEasingPointer(event);
            });
        }

        const element = document
            .getElementById("ease-canvas-view-area");

        // 削除イベント用の関数
        this._$deleteEasingPointer = this.deleteEasingPointer.bind(this);
        if (element) {

            // 非表示
            element.style.display = "none";

            // 削除イベントを無効化
            element.addEventListener("mouseleave", () =>
            {
                window
                    .removeEventListener("keydown", this._$deleteEasingPointer);
            });
        }

        const changeIds = [
            "ease-select",
            "ease-custom-file-input"
        ];

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

            const element = document.getElementById(changeIds[idx]);
            if (!element) {
                continue;
            }

            element.addEventListener("change", (event) =>
            {
                // 他のイベントを中止
                event.stopPropagation();

                // id名で関数を実行
                this.executeFunction(event.target.id, event);
            });

        }

        const elementIds = [
            "ease-custom-data-export",
            "ease-custom-data-load"
        ];

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

            const element = document.getElementById(elementIds[idx]);
            if (!element) {
                continue;
            }

            element.addEventListener("click", (event) =>
            {
                // 他のイベントを中止
                event.stopPropagation();

                // id名で関数を実行
                this.executeFunction(event.target.id, event);
            });
        }
    }

    /**
     * @description カスタムイージングのJSONデータをfile inputへ転送
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    changeEaseCustomDataLoad (event)
    {
        event.preventDefault();

        const input = document
            .getElementById("ease-custom-file-input");

        input.click();
    }

    /**
     * @description カスタムイージングの情報をJSONとしてダウンロード
     *
     * @return {void}
     * @method
     * @public
     */
    changeEaseCustomDataExport ()
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const instance = Util
            .$currentWorkSpace()
            .getLibrary(character.libraryId);
        if (!instance) {
            return ;
        }

        const anchor    = document.createElement("a");
        anchor.download = `${instance.name}_${range.startFrame}.json`;
        anchor.href     = URL.createObjectURL(new Blob(
            [JSON.stringify(character.getTween(range.startFrame).custom)],
            { "type" : "application/json" }
        ));
        anchor.click();
    }

    /**
     * @description カスタムイージングのJSONデータの取り込み実行
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    changeEaseCustomFileInput (event)
    {
        const file = event.target.files[0];

        file
            .text()
            .then((text) =>
            {
                /**
                 * @type {ArrowTool}
                 */
                const tool = Util.$tools.getDefaultTool("arrow");
                const activeElements = tool.activeElements;
                if (!activeElements.length || activeElements.length > 1) {
                    return ;
                }

                const activeElement = activeElements[0];

                const layer = Util
                    .$currentWorkSpace()
                    .scene
                    .getLayer(
                        activeElement.dataset.layerId | 0
                    );

                const character = layer.getCharacter(
                    activeElement.dataset.characterId | 0
                );
                if (!character) {
                    return ;
                }

                const range = character.getRange(
                    Util.$timelineFrame.currentFrame
                );
                if (!character.hasTween(range.startFrame)) {
                    return ;
                }

                // データの読み込み
                const tweenObject  = character.getTween(range.startFrame);
                tweenObject.custom = JSON.parse(text);

                // 変数を初期化
                this._$easeTarget = null;

                // 初期化して再生成
                this.clearEasingPointer();
                this.createEasingPointer();

                // 再描画
                this.drawEasingGraph();

                // 再計算
                this.relocationPlace(character, range.startFrame);

                // 再配置
                this
                    .clearPointer()
                    .relocationPointer();

            });

        // reset
        event.target.value = "";
    }

    /**
     * @description カスタムイージングポインターを追加
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    addEasingPointer (event)
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const parent = document
            .getElementById("ease-cubic-pointer-area");

        const children = parent.children;
        const tween    = character.getTween(range.startFrame);
        const types    = ["curve", "pointer", "curve"];
        const points   = [-20, 0, 20];

        const scale = TweenController.EASE_BASE_CANVAS_SIZE / TweenController.EASE_RANGE;

        const x = (event.layerX - TweenController.EASE_BASE_CANVAS_SIZE) / scale;
        const y = (TweenController.EASE_BASE_CANVAS_SIZE - (event.layerY - 300)) / scale;

        // new pointer
        for (let idx = 0; idx < types.length; ++idx) {

            const type = types[idx];

            const dx = x + points[idx];
            const dy = y + points[idx];

            const div = this.createEasingPointerDiv(dx, dy, type);

            parent.insertBefore(
                div, children[children.length - 1]
            );

            tween.custom.splice(-2, 0, {
                "type": type,
                "x": dx,
                "y": dy
            });
        }

        for (let idx = 0; idx < children.length; ++idx) {
            const child = children[idx];
            child.dataset.index = `${idx + 1}`;
        }

        // 変数を初期化
        this._$easeTarget = null;

        // 再描画
        this.drawEasingGraph();

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // 再配置
        this
            .clearPointer()
            .relocationPointer();
    }

    /**
     * @description イージング関数の変更
     *
     * @return {void}
     * @method
     * @public
     */
    changeEaseSelect ()
    {
        const targetFrame = Util.$timelineLayer.targetFrame;
        if (!targetFrame) {
            return ;
        }

        const element = document.getElementById("ease-select");
        if (element.value === "custom") {
            this.showCustomArea();
        } else {
            this.hideCustomArea();
        }

        const scene = Util.$currentWorkSpace().scene;
        const layer = scene.getLayer(
            targetFrame.dataset.layerId | 0
        );

        const frame = targetFrame.dataset.frame | 0;
        const characters = layer.getActiveCharacter(frame);

        if (!characters.length && characters.length > 1) {
            return ;
        }

        const character = characters[0];

        const range = character.getRange(frame);
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        character
            .getTween(range.startFrame)
            .method = element.value;

        //  tweenの座標を再計算してポインターを再配置
        character.relocationTween(range.startFrame);
    }

    /**
     * @description tweenの座標位置を再計算
     *
     * @param  {Character} character
     * @param  {number} frame
     * @param  {string} [mode="none"]
     * @return {void}
     * @method
     * @public
     */
    relocationPlace (character, frame, mode = "none")
    {
        const range = character.getRange(frame);

        // 指定レンジ以前のtweenがあれば再計算
        if (mode === "none" && range.startFrame - 1) {
            const prevRange = character.getRange(range.startFrame - 1);
            if (character.hasTween(prevRange.startFrame)) {
                this.relocationPlace(character, prevRange.startFrame, "prev");
            }
        }

        // tweenのplaceを再構築
        character.updateTweenPlace(range.startFrame, range.endFrame);

        const library = Util
            .$currentWorkSpace()
            .getLibrary(character.libraryId);

        // translate
        const instance = library.createInstance(character.getPlace(frame));

        const point = character.referencePoint;

        const w = instance.width  / 2;
        const h = instance.height / 2;

        const baseBounds = library.getBounds();
        const rectangle  = instance.getBounds();
        const baseMatrix = [
            1, 0, 0, 1,
            -w - rectangle.x - point.x,
            -h - rectangle.y - point.y
        ];

        // start params
        const startPlace  = character.getPlace(range.startFrame);
        const startMatrix = startPlace.matrix;

        const startScaleX = Math.sqrt(
            startMatrix[0] * startMatrix[0]
            + startMatrix[1] * startMatrix[1]
        );
        const startScaleY = Math.sqrt(
            startMatrix[2] * startMatrix[2]
            + startMatrix[3] * startMatrix[3]
        );

        let startRotate = Math.atan2(startMatrix[1], startMatrix[0]) * Util.$Rad2Deg;
        if (0 > startRotate) {
            startRotate += 360;
        }

        const startMultiMatrix = Util.$multiplicationMatrix(
            [startMatrix[0], startMatrix[1], startMatrix[2], startMatrix[3], 0, 0],
            baseMatrix
        );

        const startX = startMatrix[4] - (startMultiMatrix[4] + w + rectangle.x + point.x);
        const startY = startMatrix[5] - (startMultiMatrix[5] + h + rectangle.y + point.y);

        const startDiv = document
            .getElementById(`tween-marker-${character.id}-${range.startFrame}`);

        if (startDiv) {
            const bounds = Util.$boundsMatrix(baseBounds, startMatrix);
            const width  = Math.abs(Math.ceil(bounds.xMax - bounds.xMin) / 2 * Util.$zoomScale);
            const height = Math.abs(Math.ceil(bounds.yMax - bounds.yMin) / 2 * Util.$zoomScale);

            startDiv.style.left = `${Util.$offsetLeft + bounds.xMin * Util.$zoomScale + width  - 2}px`;
            startDiv.style.top  = `${Util.$offsetTop  + bounds.yMin * Util.$zoomScale + height - 2}px`;
        }

        // end params
        let endFrame = range.endFrame - 1;
        let endPlace = character.getPlace(endFrame);
        if (character.hasTween(range.endFrame)) {
            endFrame = range.endFrame;
            endPlace = character.getPlace(range.endFrame);
        }

        const endMatrix = endPlace.matrix;
        const endScaleX = Math.sqrt(
            endMatrix[0] * endMatrix[0]
            + endMatrix[1] * endMatrix[1]
        );
        const endScaleY = Math.sqrt(
            endMatrix[2] * endMatrix[2]
            + endMatrix[3] * endMatrix[3]
        );

        let endRotate = Math.atan2(endMatrix[1], endMatrix[0]) * Util.$Rad2Deg;
        if (0 > endRotate) {
            endRotate += 360;
        }

        const endMultiMatrix = Util.$multiplicationMatrix(
            [endMatrix[0], endMatrix[1], endMatrix[2], endMatrix[3], 0, 0],
            baseMatrix
        );

        const endX = endMatrix[4] - (endMultiMatrix[4] + w + rectangle.x + point.x);
        const endY = endMatrix[5] - (endMultiMatrix[5] + h + rectangle.y + point.y);

        const endDiv = document
            .getElementById(`tween-marker-${character.id}-${endFrame}`);

        if (endDiv) {
            const bounds = Util.$boundsMatrix(baseBounds, endMatrix);
            const width  = Math.abs(Math.ceil(bounds.xMax - bounds.xMin) / 2 * Util.$zoomScale);
            const height = Math.abs(Math.ceil(bounds.yMax - bounds.yMin) / 2 * Util.$zoomScale);

            endDiv.style.left = `${Util.$offsetLeft + bounds.xMin * Util.$zoomScale + width  - 2}px`;
            endDiv.style.top  = `${Util.$offsetTop  + bounds.yMin * Util.$zoomScale + height - 2}px`;
        }

        const tween = character.getTween(range.startFrame);
        const functionName = tween.method;

        // (fixed logic)
        const totalFrame = endFrame - range.startFrame;

        // diff
        const diffX = endX - startX;
        const diffY = endY - startY;

        // scale
        const diffScaleX = endScaleX - startScaleX;
        const diffScaleY = endScaleY - startScaleY;

        // rotate
        const diffRotate = endRotate - startRotate;

        // ColorTransform
        const startColorTransform = startPlace.colorTransform;
        const endColorTransform   = endPlace.colorTransform;
        const ct0 = endColorTransform[0] - startColorTransform[0];
        const ct1 = endColorTransform[1] - startColorTransform[1];
        const ct2 = endColorTransform[2] - startColorTransform[2];
        const ct3 = endColorTransform[3] - startColorTransform[3];
        const ct4 = endColorTransform[4] - startColorTransform[4];
        const ct5 = endColorTransform[5] - startColorTransform[5];
        const ct6 = endColorTransform[6] - startColorTransform[6];
        const ct7 = endColorTransform[7] - startColorTransform[7];

        const { Easing } = window.next2d.ui;

        let time = 1;
        for (let frame = range.startFrame + 1; frame < endFrame; ++frame) {

            const place = character.getPlace(frame);

            const t = time / totalFrame;
            let customValue = 0;
            if (functionName === "custom") {
                for (let idx = 0; idx < tween.custom.length; idx += 3) {

                    const curve1 = tween.custom[idx + 1];

                    let pointer = tween.custom[idx + 3];
                    if (pointer.off) {
                        idx += 3;
                        for (;;) {
                            pointer = tween.custom[idx + 3];
                            if (pointer.fixed || !pointer.off) {
                                break;
                            }
                            idx += 3;
                        }
                    }

                    if (pointer.x / TweenController.EASE_RANGE > t) {

                        const curve2 = tween.custom[idx + 2];

                        customValue = this.cubicBezier(
                            curve1.x / TweenController.EASE_RANGE,
                            curve1.y / TweenController.EASE_RANGE,
                            curve2.x / TweenController.EASE_RANGE,
                            curve2.y / TweenController.EASE_RANGE
                        )(t);

                        break;
                    }
                }
            }

            const matrix = place.matrix;

            // scale
            const xScale = !diffScaleX
                ? startScaleX
                : functionName === "custom"
                    ? diffScaleX * customValue + startScaleX
                    : Easing[functionName](time, startScaleX, diffScaleX, totalFrame);

            const yScale = !diffScaleY
                ? startScaleY
                : functionName === "custom"
                    ? diffScaleY * customValue + startScaleY
                    : Easing[functionName](time, startScaleY, diffScaleY, totalFrame);

            const rotation = !diffRotate
                ? startRotate
                : functionName === "custom"
                    ? diffRotate * customValue + startRotate
                    : Easing[functionName](time, startRotate, diffRotate, totalFrame) % 360;

            // rotation
            let radianX  = Math.atan2(matrix[1],  matrix[0]);
            let radianY  = Math.atan2(-matrix[2], matrix[3]);
            const radian = rotation * Util.$Deg2Rad;
            radianY      = radianY + radian - radianX;
            radianX      = radian;

            // new matrix
            matrix[0] = xScale  * Math.cos(radianX);
            matrix[1] = xScale  * Math.sin(radianX);
            matrix[2] = -yScale * Math.sin(radianY);
            matrix[3] = yScale  * Math.cos(radianY);

            matrix[4] = !diffX
                ? startX
                : functionName === "custom"
                    ? customValue * diffX + startX
                    : Easing[functionName](time, startX, diffX, totalFrame);

            matrix[5] = !diffY
                ? startY
                : functionName === "custom"
                    ? customValue * diffY + startY
                    : Easing[functionName](time, startY, diffY, totalFrame);

            if (tween.curve.length) {

                const baseDistance = Math.sqrt(
                    Math.pow(diffX, 2)
                    + Math.pow(diffY, 2)
                );

                const distance = Math.sqrt(
                    Math.pow(matrix[4] - startX, 2)
                    + Math.pow(matrix[5] - startY, 2)
                );

                if (distance && baseDistance) {

                    const curvePoint = this.getCurvePoint(
                        distance / baseDistance,
                        startX, startY, endX, endY,
                        tween.curve
                    );

                    if (curvePoint) {
                        matrix[4] = curvePoint.x;
                        matrix[5] = curvePoint.y;
                    }
                }
            }

            const multiMatrix = Util.$multiplicationMatrix(
                [matrix[0], matrix[1], matrix[2], matrix[3], 0, 0],
                baseMatrix
            );

            matrix[4] += multiMatrix[4] + w + rectangle.x + point.x;
            matrix[5] += multiMatrix[5] + h + rectangle.y + point.y;

            // ColorTransform
            const colorTransform = place.colorTransform;

            colorTransform[0] = !ct0
                ? startColorTransform[0]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct0 * customValue + startColorTransform[0]
                        : Easing[functionName](time, startColorTransform[0], ct0, totalFrame),
                    ColorTransformController.MIN_MULTIPLIER,
                    ColorTransformController.MAX_MULTIPLIER
                );

            colorTransform[1] = !ct1
                ? startColorTransform[1]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct1 * customValue + startColorTransform[1]
                        : Easing[functionName](time, startColorTransform[1], ct1, totalFrame),
                    ColorTransformController.MIN_MULTIPLIER,
                    ColorTransformController.MAX_MULTIPLIER
                );

            colorTransform[2] = !ct2
                ? startColorTransform[2]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct2 * customValue + startColorTransform[2]
                        : Easing[functionName](time, startColorTransform[2], ct2, totalFrame),
                    ColorTransformController.MIN_MULTIPLIER,
                    ColorTransformController.MAX_MULTIPLIER
                );

            colorTransform[3] = !ct3
                ? startColorTransform[3]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct3 * customValue + startColorTransform[3]
                        : Easing[functionName](time, startColorTransform[3], ct3, totalFrame),
                    ColorTransformController.MIN_MULTIPLIER,
                    ColorTransformController.MAX_MULTIPLIER
                );

            colorTransform[4] = !ct4
                ? startColorTransform[4]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct4 * customValue + startColorTransform[4]
                        : Easing[functionName](time, startColorTransform[4], ct4, totalFrame),
                    ColorTransformController.MIN_OFFSET,
                    ColorTransformController.MAX_OFFSET
                );

            colorTransform[5] = !ct5
                ? startColorTransform[5]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct5 * customValue + startColorTransform[5]
                        : Easing[functionName](time, startColorTransform[5], ct5, totalFrame),
                    ColorTransformController.MIN_OFFSET,
                    ColorTransformController.MAX_OFFSET
                );

            colorTransform[6] = !ct6
                ? startColorTransform[6]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct6 * customValue + startColorTransform[6]
                        : Easing[functionName](time, startColorTransform[6], ct6, totalFrame),
                    ColorTransformController.MIN_OFFSET,
                    ColorTransformController.MAX_OFFSET
                );

            colorTransform[7] = !ct7
                ? startColorTransform[7]
                : Util.$clamp(
                    functionName === "custom"
                        ? ct7 * customValue + startColorTransform[7]
                        : Easing[functionName](time, startColorTransform[7], ct7, totalFrame),
                    ColorTransformController.MIN_OFFSET,
                    ColorTransformController.MAX_OFFSET
                );

            const div = document
                .getElementById(`tween-marker-${character.id}-${frame}`);

            if (div) {
                const bounds = Util.$boundsMatrix(baseBounds, matrix);
                const width  = Math.abs(Math.ceil(bounds.xMax - bounds.xMin) / 2 * Util.$zoomScale);
                const height = Math.abs(Math.ceil(bounds.yMax - bounds.yMin) / 2 * Util.$zoomScale);

                div.style.left = `${Util.$offsetLeft + bounds.xMin * Util.$zoomScale + width  - 2}px`;
                div.style.top  = `${Util.$offsetTop  + bounds.yMin * Util.$zoomScale + height - 2}px`;
            }

            time++;
        }

        // filter
        const startFilters = startPlace.filter;
        const endFilters   = endPlace.filter;
        if (startFilters.length && endFilters.length) {

            const params = [
                "blurX",
                "blurY",
                "quality",
                "color",
                "alpha",
                "distance",
                "angle",
                "highlightColor",
                "highlightAlpha",
                "shadowColor",
                "shadowAlpha",
                "strength"
            ];

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

                const startFilter = startFilters[idx];
                const endFilter   = endFilters[idx];

                if (startFilter.name !== endFilter.name) {
                    continue;
                }

                let time = 1;
                for (let frame = range.startFrame + 1; range.endFrame > frame; ++frame) {

                    const filters = character.getPlace(frame).filter;
                    if (!filters[idx]) {
                        filters[idx] = new Util.$filterClasses[startFilter.name]();
                    }

                    const filter = filters[idx];
                    for (let idx = 0; idx < params.length; ++idx) {

                        const name = params[idx];

                        if (name in filter) {

                            const diff = endFilter[name] - startFilter[name];
                            if (!diff) {
                                continue;
                            }

                            filter[name] = functionName === "custom"
                                ? diff * customValue + startFilter[name]
                                : Easing[functionName](time, startFilter[name], diff, totalFrame);

                        }

                    }

                    time++;
                }
            }
        }

        character._$image = null;
    }

    /**
     * @description tweenのポインターをスクリーンに配置
     *
     * @return {void}
     * @method
     * @public
     */
    relocationPointer ()
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length) {
            return ;
        }

        const frame = Util.$timelineFrame.currentFrame;
        const scene = Util.$currentWorkSpace().scene;
        for (let idx = 0; idx < activeElements.length; ++idx) {

            const element = activeElements[idx];

            const layer = scene.getLayer(
                element.dataset.layerId | 0
            );

            const character = layer.getCharacter(
                element.dataset.characterId | 0
            );

            if (!character) {
                continue;
            }

            const range = character.getRange(frame);
            if (!character.hasTween(range.startFrame)) {
                continue;
            }

            let startFrame = range.startFrame;
            while (character.startFrame !== startFrame && startFrame > 1) {

                const range = character.getRange(startFrame - 1);
                if (!character.hasTween(range.startFrame)) {
                    break;
                }

                startFrame = range.startFrame;
            }

            let endFrame = range.endFrame;
            while (character.hasTween(endFrame)) {
                endFrame = character.getTween(endFrame).endFrame;
            }

            const instance = Util
                .$currentWorkSpace()
                .getLibrary(character.libraryId);

            const baseBounds = instance.getBounds();
            const parentElement = document.getElementById("stage-area");
            for (let frame = startFrame; frame < endFrame; ++frame) {

                const div = document.createElement("div");

                // 表示座標
                const matrix = character.getPlace(frame).matrix;
                const bounds = Util.$boundsMatrix(baseBounds, matrix);
                const width  = Math.abs(Math.ceil(bounds.xMax - bounds.xMin) / 2 * Util.$zoomScale);
                const height = Math.abs(Math.ceil(bounds.yMax - bounds.yMin) / 2 * Util.$zoomScale);
                div.style.left = `${Util.$offsetLeft + bounds.xMin * Util.$zoomScale + width  - 2}px`;
                div.style.top  = `${Util.$offsetTop  + bounds.yMin * Util.$zoomScale + height - 2}px`;

                // 表示用データ
                div.id = `tween-marker-${character.id}-${frame}`;
                div.classList.add("tween-marker");
                div.dataset.child = "tween";

                parentElement.appendChild(div);
            }

            const tweenObject = character.getTween(range.startFrame);
            for (let idx = 0; idx < tweenObject.curve.length; ++idx) {

                const pointer = tweenObject.curve[idx];

                parentElement.appendChild(
                    this.createTweenCurveElement(pointer, idx, layer.id)
                );

            }
        }
    }

    /**
     * @description スクリーンのポインターを非表示にする
     *
     * @return {TweenController}
     * @method
     * @public
     */
    clearPointer ()
    {
        const element = document
            .getElementById("stage-area");

        if (!element) {
            return this;
        }

        let idx = 0;
        while (element.children.length > idx) {

            const node = element.children[idx];
            if (node.dataset.child !== "tween") {
                idx++;
                continue;
            }

            node.remove();
        }

        return this;
    }

    /**
     * @description カーブポインターの移動開始関数
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    startMoveCurvePointer (event)
    {
        if (event.button) {
            return ;
        }

        Util.$endMenu();

        // 他のイベントを中止
        event.stopPropagation();

        this._$currentTarget = event.target;

        this._$pointX = event.pageX;
        this._$pointY = event.pageY;

        if (!this._$moveCurvePointer) {
            this._$moveCurvePointer = this.moveCurvePointer.bind(this);
        }

        if (!this._$endMoveCurvePointer) {
            this._$endMoveCurvePointer = this.endMoveCurvePointer.bind(this);
        }

        // 保存開始
        this.save();

        window.addEventListener("mousemove", this._$moveCurvePointer);
        window.addEventListener("mouseup", this._$endMoveCurvePointer);
    }

    /**
     * @description カーブポインターを削除
     *
     * @return {void}
     * @method
     * @public
     */
    deleteCurvePointer ()
    {
        const element = this._$currentTarget;
        if (!element) {
            return ;
        }

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                element.dataset.layerId | 0
            );

        const frame = Util.$timelineFrame.currentFrame;
        const characters = layer.getActiveCharacter(frame);

        if (!characters.length || characters.length > 1) {
            return ;
        }

        // set select
        const character = characters[0];
        const range = character.getRange(frame);

        // カーブポインターを削除
        character
            .getTween(range.startFrame)
            .curve
            .splice(element.dataset.index | 0, 1);

        // カーブElementを削除
        element.remove();

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // 再配置
        this
            .clearPointer()
            .relocationPointer();

        this.save();

        // 初期化
        this._$saved = false;
        this._$currentTarget = null;
    }

    /**
     * @description カーブポインターの移動関数
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    moveCurvePointer (event)
    {
        // 全てのイベントを中止
        event.preventDefault();

        window.requestAnimationFrame(() =>
        {
            const element = this._$currentTarget;
            if (!element) {
                return ;
            }

            const x = (event.pageX - this._$pointX) / Util.$zoomScale;
            const y = (event.pageY - this._$pointY) / Util.$zoomScale;

            const layer = Util
                .$currentWorkSpace()
                .scene
                .getLayer(
                    element.dataset.layerId | 0
                );

            const frame = Util.$timelineFrame.currentFrame;

            const characters = layer.getActiveCharacter(frame);
            if (!characters.length && characters.length > 1) {
                return ;
            }

            // set select
            const character = characters[0];

            // tweenがなければ終了
            const range = character.getRange(frame);
            if (!character.hasTween(range.startFrame)) {
                return ;
            }

            const point = character
                .getTween(range.startFrame)
                .curve[element.dataset.index];

            point.x += x;
            point.y += y;

            // 再計算
            this.relocationPlace(character, range.startFrame);

            // 再配置
            this
                .clearPointer()
                .relocationPointer();

            // 再描画
            this.reloadScreen();

            this._$pointX = event.pageX;
            this._$pointY = event.pageY;
        });
    }

    /**
     * @description カーブポインターの移動終了
     *
     * @return {void}
     * @method
     * @public
     */
    endMoveCurvePointer ()
    {
        // イベントを終了
        window.removeEventListener("mousemove", this._$moveCurvePointer);
        window.removeEventListener("mouseup", this._$endMoveCurvePointer);

        // 初期化
        this._$saved = false;
    }

    /**
     * @description カーブポインターのアクティブon/off
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    switchingCurvePointer (event)
    {
        const element = event.target;

        const scene = Util.$currentWorkSpace().scene;
        const layer = scene.getLayer(element.dataset.layerId | 0);

        const frame = Util.$timelineFrame.currentFrame;

        const characters = layer.getActiveCharacter(frame);
        if (!characters.length && characters.length > 1) {
            return ;
        }

        // set select
        const character = characters[0];

        // tweenがなければ終了
        const range = character.getRange(frame);
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const pointer = character
            .getTween(range.startFrame)
            .curve[element.dataset.index];

        pointer.usePoint = !pointer.usePoint;

        if (pointer.usePoint) {

            element.classList.remove("tween-pointer-disabled");
            element.classList.add("tween-pointer-active");

        } else {

            element.classList.add("tween-pointer-disabled");
            element.classList.remove("tween-pointer-active");

        }

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // ポインターを再配置
        this
            .clearPointer()
            .relocationPointer();

        // 再描画
        this.reloadScreen();
    }

    /**
     * @param  {object} pointer
     * @param  {number} index
     * @param  {number} layerId
     * @return {HTMLDivElement|null}
     * @method
     * @public
     */
    createTweenCurveElement (pointer, index, layerId)
    {
        const div = document.createElement("div");
        div.classList.add(
            "tween-pointer-marker",
            "tween-pointer-disabled"
        );

        const frame = Util.$timelineFrame.currentFrame;

        div.textContent     = `${index + 1}`;
        div.dataset.child   = "tween";
        div.dataset.curve   = "true";
        div.dataset.layerId = `${layerId}`;
        div.dataset.index   = `${index}`;
        div.dataset.detail  = "{{カーブポインター(ダブルクリックでON/OFF)}}";

        const scene = Util.$currentWorkSpace().scene;
        const layer = scene.getLayer(layerId);

        const characters = layer.getActiveCharacter(frame);
        if (!characters.length || characters.length > 1) {
            return null;
        }

        const character = characters[0];

        const bounds = Util
            .$currentWorkSpace()
            .getLibrary(character.libraryId)
            .getBounds();

        const width  = Math.abs(Math.ceil(bounds.xMax - bounds.xMin) / 2 * Util.$zoomScale);
        const height = Math.abs(Math.ceil(bounds.yMax - bounds.yMin) / 2 * Util.$zoomScale);

        div.style.left = `${Util.$offsetLeft + pointer.x * Util.$zoomScale + width  - 7}px`;
        div.style.top  = `${Util.$offsetTop  + pointer.y * Util.$zoomScale + height - 7}px`;

        if (pointer.usePoint) {
            div.classList.remove("tween-pointer-disabled");
            div.classList.add("tween-pointer-active");
        } else {
            div.classList.add("tween-pointer-disabled");
            div.classList.remove("tween-pointer-active");
        }

        div.addEventListener("mousedown", (event) =>
        {
            this.startMoveCurvePointer(event);
        });

        div.addEventListener("dblclick", (event) =>
        {
            this.switchingCurvePointer(event);
        });

        div.addEventListener("mouseover", Util.$fadeIn);
        div.addEventListener("mouseout",  Util.$fadeOut);

        return div;
    }

    /**
     * @description イージングポインターの初期オブジェクト
     *
     * @return {object}
     * @method
     * @public
     */
    createEasingObject ()
    {
        return [
            {
                "type": "pointer",
                "fixed": true,
                "x": 0,
                "y": 0
            },
            {
                "type": "curve",
                "x": 0,
                "y": 0
            },
            {
                "type": "curve",
                "x": 100,
                "y": 100
            },
            {
                "type": "pointer",
                "fixed": true,
                "x": 100,
                "y": 100
            }
        ];
    }

    /**
     * @description カスタムイージングのポインターを生成
     *
     * @return {void}
     * @method
     * @public
     */
    createEasingPointer ()
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const tweenObject = character.getTween(range.startFrame);

        const element = document
            .getElementById("ease-cubic-pointer-area");

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

            const custom = tweenObject.custom[idx];
            if (custom.fixed) {
                continue;
            }

            const div = this.createEasingPointerDiv(
                custom.x, custom.y, custom.type, idx
            );

            if (custom.off) {
                div.classList.add("ease-cubic-disable");
            }

            element.appendChild(div);
        }
    }

    /**
     * @description カスタムイージングのポインターの移動開始処理
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    startMoveEasingPointer (event)
    {
        this._$easeTarget = event.currentTarget;
        this._$pointX     = event.screenX;
        this._$pointY     = event.screenY;

        if (!this._$moveEasingPointer) {
            this._$moveEasingPointer = this.moveEasingPointer.bind(this);
        }

        if (!this._$endMoveEasingPointer) {
            this._$endMoveEasingPointer = this.endMoveEasingPointer.bind(this);
        }

        this.save();

        // イベントを登録
        window.addEventListener("mousemove", this._$moveEasingPointer);
        window.addEventListener("mouseup", this._$endMoveEasingPointer);
        window.removeEventListener("keydown", this._$deleteEasingPointer);
    }

    /**
     * @description カスタムイージングのポインターを削除
     *
     * @param  {KeyboardEvent} event
     * @return {void}
     * @method
     * @public
     */
    deleteEasingPointer (event)
    {
        if (event.key !== "Backspace" || !this._$easeTarget) {
            return ;
        }

        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const tweenObject = character.getTween(range.startFrame);
        const index  = this._$easeTarget.dataset.index | 0;
        tweenObject.custom.splice(index - 1, 3);

        const children = document
            .getElementById("ease-cubic-pointer-area")
            .children;

        children[index].remove();
        children[index - 1].remove();
        children[index - 2].remove();

        for (let idx = 0; idx < children.length; ++idx) {
            const child = children[idx];
            child.dataset.index = `${idx + 1}`;
        }

        // 再描画
        this.drawEasingGraph();

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // 再配置
        this
            .clearPointer()
            .relocationPointer();
    }

    /**
     * @description カスタムイージングのポインターの移動関数
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    moveEasingPointer (event)
    {
        window.requestAnimationFrame(() =>
        {
            if (!this._$easeTarget) {
                return ;
            }

            /**
             * @type {ArrowTool}
             */
            const tool = Util.$tools.getDefaultTool("arrow");
            const activeElements = tool.activeElements;
            if (!activeElements.length || activeElements.length > 1) {
                return ;
            }

            const activeElement = activeElements[0];

            const layer = Util
                .$currentWorkSpace()
                .scene
                .getLayer(
                    activeElement.dataset.layerId | 0
                );

            const character = layer.getCharacter(
                activeElement.dataset.characterId | 0
            );
            if (!character) {
                return ;
            }

            const range = character.getRange(
                Util.$timelineFrame.currentFrame
            );
            if (!character.hasTween(range.startFrame)) {
                return ;
            }

            const element = this._$easeTarget;
            let x = element.offsetLeft + event.screenX - this._$pointX;
            let y = element.offsetTop  + event.screenY - this._$pointY;

            // update
            this._$pointX = event.screenX;
            this._$pointY = event.screenY;

            if (TweenController.EASE_MIN_POINTER_Y > y) {
                y = TweenController.EASE_MIN_POINTER_Y;
            }

            if (TweenController.EASE_MAX_POINTER_Y < y) {
                y = TweenController.EASE_MAX_POINTER_Y;
            }

            if (TweenController.EASE_MIN_POINTER_X > x) {
                x = TweenController.EASE_MIN_POINTER_X;
            }

            if (TweenController.EASE_MAX_POINTER_X < x) {
                x = TweenController.EASE_MAX_POINTER_X;
            }

            element.style.left = `${x}px`;
            element.style.top  = `${y}px`;

            const tweenObject = character.getTween(range.startFrame);
            const custom = tweenObject.custom[element.dataset.index];

            const scale = TweenController.EASE_BASE_CANVAS_SIZE / TweenController.EASE_RANGE;
            custom.x = (x - TweenController.EASE_SCREEN_X) / scale;
            custom.y = (TweenController.EASE_MOVE_Y - y) / scale;

            document
                .getElementById("ease-cubic-current-text")
                .textContent = `(${custom.x / TweenController.EASE_RANGE * 100 | 0})`;

            document
                .getElementById("ease-cubic-current-tween")
                .textContent = `(${custom.y / TweenController.EASE_RANGE * 100 | 0})`;

            // 再描画
            this.drawEasingGraph();

            // 再計算
            this.relocationPlace(character, range.startFrame);

            // 再配置
            this
                .clearPointer()
                .relocationPointer();
        });
    }

    /**
     * @description カスタムイージングのポインターの移動を終了
     *
     * @return {void}
     * @method
     * @public
     */
    endMoveEasingPointer ()
    {
        // イベントを登録
        window.removeEventListener("mousemove", this._$moveEasingPointer);
        window.removeEventListener("mouseup", this._$endMoveEasingPointer);
        window.addEventListener("keydown", this._$deleteEasingPointer);

        // 初期化
        this._$saved = false;
    }

    /**
     * @description カスタムイージングポインターを削除
     *
     * @param  {MouseEvent} event
     * @return {void}
     * @method
     * @public
     */
    disabledEasingPointer (event)
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const tweenObject = character.getTween(range.startFrame);

        const index  = event.target.dataset.index | 0;
        const custom = tweenObject.custom[index];

        custom.off = !custom.off;
        tweenObject.custom[index - 1].off = custom.off;
        tweenObject.custom[index + 1].off = custom.off;

        const children = document
            .getElementById("ease-cubic-pointer-area")
            .children;

        if (custom.off) {
            children[index - 2].classList.add("ease-cubic-disable");
            children[index - 1].classList.add("ease-cubic-disable");
            children[index    ].classList.add("ease-cubic-disable");
        } else {
            children[index - 2].classList.remove("ease-cubic-disable");
            children[index - 1].classList.remove("ease-cubic-disable");
            children[index    ].classList.remove("ease-cubic-disable");
        }

        // 変数を初期化
        this._$easeTarget = null;

        // 再描画
        this.drawEasingGraph();

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // 再配置
        this
            .clearPointer()
            .relocationPointer();
    }

    /**
     * @description カスタムイージングのdivを生成
     *
     * @param  {number}  [x=0]
     * @param  {number}  [y=0]
     * @param  {string}  [type="pointer"]
     * @param  {number}  [index=0]
     * @return {HTMLDivElement}
     * @method
     * @public
     */
    createEasingPointerDiv (x = 0, y = 0, type = "pointer", index = 0)
    {
        const div = document.createElement("div");

        // 移動開始イベント
        div.addEventListener("mousedown", (event) =>
        {
            this.startMoveEasingPointer(event);
        });

        if (type === "pointer") {
            // ポインターを非アクティブ化
            div.addEventListener("dblclick", (event) =>
            {
                this.disabledEasingPointer(event);
            });
        }

        div.classList.add(`ease-cubic-${type}`);
        div.dataset.index = `${index}`;
        div.dataset.type  = `${type}`;

        const scale = TweenController.EASE_BASE_CANVAS_SIZE / TweenController.EASE_RANGE;

        div.style.left = `${TweenController.EASE_SCREEN_X + x * scale}px`;
        div.style.top  = `${TweenController.EASE_SCREEN_Y + (TweenController.EASE_RANGE - y) * scale}px`;

        return div;
    }

    /**
     * @description カスタムイージングの状態を描画
     *
     * @return {void}
     * @method
     * @public
     */
    drawEasingGraph ()
    {
        /**
         * @type {ArrowTool}
         */
        const tool = Util.$tools.getDefaultTool("arrow");
        const activeElements = tool.activeElements;
        if (!activeElements.length || activeElements.length > 1) {
            return ;
        }

        const activeElement = activeElements[0];

        const layer = Util
            .$currentWorkSpace()
            .scene
            .getLayer(
                activeElement.dataset.layerId | 0
            );

        const character = layer.getCharacter(
            activeElement.dataset.characterId | 0
        );
        if (!character) {
            return ;
        }

        const range = character.getRange(
            Util.$timelineFrame.currentFrame
        );
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const tweenObject = character.getTween(range.startFrame);

        const ratio   = window.devicePixelRatio;
        const offsetX = TweenController.EASE_OFFSET_X * ratio;
        const offsetY = TweenController.EASE_OFFSET_Y * ratio;

        const ctx = this._$drawContext;
        ctx.fillStyle = "rgb(240, 240, 240)";

        const size = TweenController.EASE_BASE_CANVAS_SIZE * ratio;
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        ctx.fillRect(offsetX, offsetY, size, size);

        ctx.lineCap = "round";
        ctx.translate(offsetX, offsetY);

        // ベースの描画
        ctx.beginPath();
        ctx.strokeStyle = "rgba(200, 200, 200, 0.6)";
        ctx.lineWidth   = 10;
        ctx.moveTo(0, 0);
        ctx.lineTo(size, size);
        ctx.stroke();

        const scale = TweenController.EASE_BASE_CANVAS_SIZE
            / TweenController.EASE_RANGE * ratio;

        for (let idx = 0; idx < tweenObject.custom.length; idx += 3) {

            const startPointer = tweenObject.custom[idx    ];
            const startCurve   = tweenObject.custom[idx + 1];
            let endCurve       = tweenObject.custom[idx + 2];
            let endPointer     = tweenObject.custom[idx + 3];

            if (endPointer.off) {
                idx += 3;
                for (;;) {
                    endCurve   = tweenObject.custom[idx + 2];
                    endPointer = tweenObject.custom[idx + 3];
                    if (endPointer.fixed || !endPointer.off) {
                        break;
                    }
                    idx += 3;
                }
            }

            // start line
            ctx.beginPath();
            ctx.strokeStyle = "rgb(160, 160, 160)";
            ctx.lineWidth   = 3;
            ctx.moveTo(startPointer.x * scale, startPointer.y * scale);
            ctx.lineTo(startCurve.x * scale, startCurve.y * scale);
            ctx.stroke();

            // end line
            ctx.beginPath();
            ctx.strokeStyle = "rgb(160, 160, 160)";
            ctx.lineWidth   = 3;
            ctx.moveTo(endPointer.x * scale, endPointer.y * scale);
            ctx.lineTo(endCurve.x * scale, endCurve.y * scale);
            ctx.stroke();

            // bezier curve
            ctx.beginPath();
            ctx.strokeStyle = "rgb(80, 80, 80)";
            ctx.lineWidth   = 10;
            ctx.moveTo(startPointer.x * scale, startPointer.y * scale);
            ctx.bezierCurveTo(
                startCurve.x * scale, startCurve.y * scale,
                endCurve.x * scale, endCurve.y * scale,
                endPointer.x * scale, endPointer.y * scale
            );
            ctx.stroke();

            if (endPointer.fixed) {
                break;
            }
        }

        const viewContext = this._$viewContext;

        // clear
        viewContext.setTransform(1, 0, 0, 1, 0, 0);
        viewContext.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // 反転して出力
        viewContext.scale(1, -1);
        viewContext.translate(0, -ctx.canvas.height);
        viewContext.drawImage(ctx.canvas, 0, 0);
    }

    /**
     * @description カーブポインターのxy座標計算
     *
     * @param  {number} d
     * @param  {number} sx
     * @param  {number} sy
     * @param  {number} ex
     * @param  {number} ey
     * @param  {array} curves
     * @return {object}
     * @method
     * @public
     */
    getCurvePoint (d, sx, sy, ex, ey, curves)
    {
        const targets = [];
        for (let idx = 0; idx < curves.length; ++idx) {

            const pointer = curves[idx];

            if (!pointer.usePoint) {
                continue;
            }

            targets.push(pointer);
        }

        if (!targets.length) {
            return null;
        }

        const t = 1 - d;
        const l = targets.length + 1;
        for (let idx = 0; idx < l; ++idx) {
            sx *= t;
            sy *= t;
            ex *= d;
            ey *= d;
        }

        let x = sx + ex;
        let y = sy + ey;
        for (let idx = 0; idx < targets.length; ++idx) {

            const curve = targets[idx];

            const p = idx + 1;

            let cx = curve.x * l;
            let cy = curve.y * l;
            for (let jdx = 0; jdx < p; ++jdx) {
                cx *= d;
                cy *= d;
            }

            for (let jdx = 0; jdx < l - p; ++jdx) {
                cx *= t;
                cy *= t;
            }

            x += cx;
            y += cy;
        }

        return {
            "x": x,
            "y": y
        };
    }

    /**
     * @description 3次ベジェのカーブの計算
     *
     * @param  {number} $x1
     * @param  {number} $y1
     * @param  {number} $x2
     * @param  {number} $y2
     * @return {function}
     * @method
     * @public
     */
    cubicBezier ($x1, $y1, $x2, $y2)
    {
        const cx = 3 * $x1,
            bx = 3 * ($x2 - $x1) - cx,
            ax = 1 - cx - bx;

        const cy = 3 * $y1,
            by = 3 * ($y2 - $y1) - cy,
            ay = 1 - cy - by;

        const bezierX = ($t) =>
        {
            return $t * (cx + $t * (bx + $t * ax));
        };

        const bezierDX = ($t) =>
        {
            return cx + $t * (2 * bx + 3 * ax * $t);
        };

        const newtonRaphson = ($x) =>
        {
            if ($x <= 0) {
                return 0;
            }

            if ($x >= 1) {
                return 1;
            }

            let limit = 0;
            let prev = 0, t = $x;
            while (Math.abs(t - prev) > 1e-4) {

                prev = t;
                t = t - (bezierX(t) - $x) / bezierDX(t);

                limit++;
                if (limit > 1000) {
                    break;
                }
            }

            return t;
        };

        return ($t) =>
        {
            const t = newtonRaphson($t);
            return t * (cy + t * (by + t * ay));
        };
    }

    /**
     * @description カスタムイージングポインターを初期化
     *
     * @return {TweenController}
     * @method
     * @public
     */
    clearEasingPointer ()
    {
        // 初期化
        const element = document
            .getElementById("ease-cubic-pointer-area");

        if (element) {
            while (element.children.length) {
                element.children[0].remove();
            }
        }
    }

    /**
     * @description イージングコントローラーを表示
     *
     * @return {TweenController}
     * @method
     * @public
     */
    showCustomArea ()
    {
        // 初期化
        this.clearEasingPointer();
        this._$easeTarget = null;

        document
            .getElementById("ease-canvas-view-area")
            .style.display = "";

        this.createEasingPointer();
        this.drawEasingGraph();
    }

    /**
     * @description イージングコントローラーを非表示
     *
     * @return {void}
     * @method
     * @public
     */
    hideCustomArea ()
    {
        const element = document
            .getElementById("ease-canvas-view-area");

        if (element) {
            element.style.display = "none";
        }
    }

    /**
     * @description tweenのカーブポイントを追加
     *
     * @return {void}
     * @method
     * @public
     */
    addCurvePinter ()
    {
        const targetLayer = Util.$timelineLayer.targetLayer;
        if (!targetLayer) {
            return ;
        }

        const frame = Util.$timelineFrame.currentFrame;

        const scene = Util.$currentWorkSpace().scene;
        const layer = scene.getLayer(
            targetLayer.dataset.layerId | 0
        );

        const characters = layer.getActiveCharacter(frame);
        if (!characters.length || characters.length > 1) {
            return ;
        }

        const character = characters[0];

        const range = character.getRange(frame);
        if (!character.hasTween(range.startFrame)) {
            return ;
        }

        const tweenObject = character.getTween(range.startFrame);

        const index  = tweenObject.curve.length;
        const bounds = character.getBounds();

        const pointer = {
            "usePoint": true,
            "x": bounds.xMin,
            "y": bounds.yMin
        };
        tweenObject.curve.push(pointer);

        const div = this.createTweenCurveElement(pointer, index, layer.id);
        if (div) {
            document
                .getElementById("stage-area")
                .appendChild(div);
        }

        // 再計算
        this.relocationPlace(character, range.startFrame);

        // ポインターを再配置
        this
            .clearPointer()
            .relocationPointer();
    }
}

Util.$tweenController = new TweenController();