(function ($) {
    //jquery 拡張

    //アニメーション開始。未実装　キーフレアニメは投入したい
    $.fn.a2d = function () {
        return this.each(function (i) {
            $(this).css("-webkit-animation-play-state", str);
        });
    };

    $.getBaseURL = function () {
        var str = location.pathname;
        var i = str.lastIndexOf("/");
        return str.substring(0, i + 1);
    };

    $.getDirPath = function (str) {
        var i = str.lastIndexOf("/");
        return str.substring(0, i + 1);
    };

    $.isHTTP = function (str) {
        if (str.substring(0, 4) === "http") {
            return true;
        } else {
            return false;
        }
    };

    $.play_audio = function (audio_obj) {
        audio_obj.play();
    };

    $.toBoolean = function (str) {
        if (str == "true") {
            return true;
        } else {
            return false;
        }
    };

    $.getAngle = function () {
        let angle = screen && screen.orientation && screen.orientation.angle;
        if (angle === undefined) {
            angle = window.orientation; // iOS用
        }

        return angle;
    };

    //横幅の方が大きければtrue;
    $.getLargeScreenWidth = function () {
        let w = parseInt(window.innerWidth);
        let h = parseInt(window.innerHeight);

        if (w > h) {
            return true;
        } else {
            return false;
        }
    };

    $.localFilePath = function () {
        var path = "";
        //Mac os Sierra 対応
        if (process.execPath.indexOf("var/folders") != -1) {
            path = process.env.HOME + "/_TyranoGameData";
        } else {
            path = $.getExePath();
        }

        return path;
    };

    $.getViewPort = function () {
        var width, heiht;

        if (self.innerHeight) {
            // all except Explorer
            width = self.innerWidth;
            height = self.innerHeight;
        } else if (document.documentElement && document.documentElement.clientHeight) {
            // Explorer 6 Strict Mode
            width = document.documentElement.clientWidth;
            height = document.documentElement.clientHeight;
        } else if (document.body) {
            // other Explorers
            width = document.body.clientWidth;
            height = document.body.clientHeight;
        }

        return {
            width: width,
            height: height,
        };
    };

    $.escapeHTML = function (val, replace_str) {
        val = val || "";
        var t = $("<div />").text(val).html();

        if (replace_str) {
            if (t === "") {
                t = replace_str;
            }
        }
        return t;
    };

    $.br = function (txtVal) {
        txtVal = txtVal.replace(/\r\n/g, "<br />");
        txtVal = txtVal.replace(/(\n|\r)/g, "<br />");
        return txtVal;
    };

    const dateFormatter = {
        _fmt: {
            hh: function (date) {
                return ("0" + date.getHours()).slice(-2);
            },
            h: function (date) {
                return date.getHours();
            },
            mm: function (date) {
                return ("0" + date.getMinutes()).slice(-2);
            },
            m: function (date) {
                return date.getMinutes();
            },
            ss: function (date) {
                return ("0" + date.getSeconds()).slice(-2);
            },
            dd: function (date) {
                return ("0" + date.getDate()).slice(-2);
            },
            d: function (date) {
                return date.getDate();
            },
            s: function (date) {
                return date.getSeconds();
            },
            yyyy: function (date) {
                return date.getFullYear() + "";
            },
            yy: function (date) {
                return date.getYear() + "";
            },
            t: function (date) {
                return date.getDate() <= 3 ? ["st", "nd", "rd"][date.getDate() - 1] : "th";
            },
            w: function (date) {
                return ["Sun", "$on", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()];
            },
            MMMM: function (date) {
                return [
                    "January",
                    "February",
                    "$arch",
                    "April",
                    "$ay",
                    "June",
                    "July",
                    "August",
                    "September",
                    "October",
                    "November",
                    "December",
                ][date.getMonth()];
            },
            MMM: function (date) {
                return ["Jan", "Feb", "$ar", "Apr", "$ay", "Jun", "Jly", "Aug", "Spt", "Oct", "Nov", "Dec"][date.getMonth()];
            },
            MM: function (date) {
                return ("0" + (date.getMonth() + 1)).slice(-2);
            },
            M: function (date) {
                return date.getMonth() + 1;
            },
            $: function (date) {
                return "M";
            },
        },
        _priority: ["hh", "h", "mm", "m", "ss", "dd", "d", "s", "yyyy", "yy", "t", "w", "MMMM", "MMM", "MM", "M", "$"],
        format: function (date, format) {
            return this._priority.reduce((res, fmt) => res.replace(fmt, this._fmt[fmt](date)), format);
        },
    };

    /**
     * Dateをフォーマットする
     * @param {Date} date 日付
     * @param {string} format フォーマット (例) "yyyy/M/d hh:mm:ss"
     * @returns {string}
     */
    $.formatDate = function (date, format) {
        return dateFormatter.format(date, format);
    };

    //現在時刻を取得
    //現在の日
    $.getNowDate = function (format = "yyyy/M/d") {
        return $.formatDate(new Date(), format);
    };

    //現在の時刻
    $.getNowTime = function (format = "hh：mm：ss") {
        return $.formatDate(new Date(), format);
    };

    $.convertRem = function (px_val) {
        function getRootElementFontSize() {
            // Returns a number
            return parseFloat(
                // of the computed font-size, so in px
                getComputedStyle(
                    // for the root <html> element
                    document.documentElement,
                ).fontSize,
            );
        }

        return px_val * getRootElementFontSize();
    };

    //複数のスクリプトを一括して読み込み
    $.getMultiScripts = function (arr, cb) {
        var cnt_script = arr.length;
        var load_cnt = 0;

        if (cnt_script == 0) {
            cb();
            return;
        }

        function getScript(src) {
            $.getScript(arr[load_cnt], function (e) {
                load_cnt++;

                if (cnt_script == load_cnt) {
                    if (typeof cb == "function") {
                        cb();
                    }
                } else {
                    getScript(arr[load_cnt]);
                }
            });
        }

        getScript(arr[0]);
    };

    $.convertSecToString = function (val) {
        if (val == 0) {
            return "-";
        }
        var day = Math.floor(val / (24 * 60 * 60));
        var hour = Math.floor((val % (24 * 60 * 60)) / (60 * 60));
        var minute = Math.floor(((val % (24 * 60 * 60)) % (60 * 60)) / 60);
        var second = Math.floor(((val % (24 * 60 * 60)) % (60 * 60)) % 60);

        var str = "";
        if (day !== 0) {
            str += day + "日";
        }
        if (hour !== 0) {
            str += hour + "時間";
        }
        if (minute !== 0) {
            str += minute + "分";
        }
        if (second !== 0) {
            str += second + "秒";
        }

        return str;
    };

    $.secToMinute = function (val) {
        if (val === 0) {
            return "-";
        }

        var m = Math.floor(val / 60);
        var s = Math.floor(val % 60);
        var str = "";

        if (m !== 0) {
            str += m + "分";
        }
        str += s + "秒";

        return str;
    };

    $.trim = function (str) {
        if (str) {
        } else {
            return "";
        }

        return str.replace(/^\s+|\s+$/g, "");
    };

    $.rmspace = function (str) {
        str = str.replace(/ /g, "");
        str = str.replace(/　/g, "");
        str = str.replace(/\r\n?/g, "");

        return str;
    };

    $.replaceAll = function (text, searchString, replacement) {
        if (typeof text != "string") {
            return text;
        }

        //置換のコード変えてみた
        var result = text.split(searchString).join(replacement);

        return result;
    };

    //確証しを取得
    $.getExt = function (str) {
        return str.split(".").pop();
    };

    //指定した拡張子を付ける。拡張子がなければ
    $.setExt = function (name, ext_str) {
        var tmp = name.split(".");
        if (tmp.length == 1) {
            name = name + "." + ext_str;
        }

        return name;
    };

    //要素をクローンします
    $.cloneObject = function (source) {
        return $.extend(true, {}, source);
    };

    //透明度を適切な値に変更
    $.convertOpacity = function (val) {
        //255をマックスとして計算する

        var p = val / 255;

        return p;
    };

    //パスにfgimage bgimage image が含まれていた場合、それを適応する
    $.convertStorage = function (path) {};

    $.convertColor = function (val) {
        if (val.indexOf("0x") != -1) {
            return val.replace("0x", "#");
        }

        return val;
    };

    $.convertBold = function (flag) {
        if (flag == "true") {
            return "bold";
        }

        return "";
    };

    $.convertItalic = function (flag) {
        if (flag == "true") {
            return "italic";
        }

        return "";
    };

    $.send = function (url, obj, call_back) {
        //game.current_story_file = story_file;
        $.ajax({
            type: "POST",
            url: url,
            data: obj,
            dataType: "json",
            complete: function () {
                //通信終了時の処理
                $.hideLoading();
            },
            success: function (data, status) {
                $.hideLoading();

                var data_obj = data;
                if (call_back) {
                    call_back(data_obj);
                }
            },
        });
    };

    $.loadText = function (file_path, callback) {
        /*
        var httpObj = jQuery.get(file_path + "?" + Math.floor(Math.random() * 1000000), null, function(obj) {

            var order_str = "";

            if (httpObj) {
                if (httpObj.responseText) {
                    order_str = httpObj.responseText;
                } else {

                    order_str = obj;

                }
            } else {

                order_str = obj;

            }

            callback(order_str);
            // createOrder
        });

        */

        $.ajax({
            url: file_path + "?" + Math.floor(Math.random() * 1000000),
            cache: false,
            success: function (text) {
                order_str = text;
                callback(order_str);
            },
            error: function () {
                alert("file not found:" + file_path);
                callback("");
            },
        });
    };

    //クッキーを取得
    $.getCookie = function (key) {
        var tmp = document.cookie + ";";
        var index1 = tmp.indexOf(key, 0);
        if (index1 != -1) {
            tmp = tmp.substring(index1, tmp.length);
            var index2 = tmp.indexOf("=", 0) + 1;
            var index3 = tmp.indexOf(";", index2);
            return tmp.substring(index2, index3);
        }
        return null;
    };

    $.isNull = function (str) {
        if (str == null) {
            return "";
        } else {
        }

        return str;
    };

    $.dstop = function () {
        console.log("dstop");
    };

    //ユーザ環境を取得
    $.userenv = function () {
        var ua = navigator.userAgent;
        if (ua.indexOf("iPhone") > -1) {
            return "iphone";
        } else if (ua.indexOf("iPad") > -1) {
            return "iphone";
        } else if (ua.indexOf("Android") > -1) {
            return "android";
        } else if (ua.indexOf("Chrome") > -1 && navigator.platform.indexOf("Linux") > -1) {
            return "android";
        } else {
            return "pc";
        }
    };

    $.isTyranoPlayer = function () {
        if (typeof _tyrano_player != "undefined") {
            return true;
        } else {
            return false;
        }
    };

    $.lang = function (key) {
        if (tyrano_lang["word"][key]) {
            return tyrano_lang["word"][key];
        } else {
            return "NOT_DEFINED";
        }
    };

    $.novel = function (key) {
        if (tyrano_lang["novel"][key]) {
            return tyrano_lang["novel"][key];
        } else {
            return "NOT_DEFINED";
        }
    };

    //ユーザのブラウザ情報を取得
    $.getBrowser = function () {
        var userAgent = window.navigator.userAgent.toLowerCase();

        if (userAgent.indexOf("msie") >= 0 || userAgent.indexOf("trident") >= 0) {
            return "msie";
        } else if (userAgent.indexOf("edge") > -1) {
            return "edge";
        } else if (userAgent.indexOf("firefox") > -1) {
            return "firefox";
        } else if (userAgent.indexOf("opera") > -1) {
            return "opera";
        } else if (userAgent.indexOf("chrome") > -1) {
            return "chrome";
        } else if (userAgent.indexOf("safari") > -1) {
            return "safari";
        } else if (userAgent.indexOf("applewebkit") > -1) {
            return "safari";
        } else {
            return "unknown";
        }
    };

    ($.isNWJS = function () {
        //Electronならfalse
        if ($.isElectron()) {
            return false;
        }

        // Node.js で動作しているか
        var isNode = typeof process !== "undefined" && typeof require !== "undefined";
        // ブラウザ上(非Node.js)で動作しているか
        var isBrowser = !isNode;
        // node-webkitで動作しているか
        var isNodeWebkit;
        try {
            isNodeWebkit = isNode ? typeof require("nw.gui") !== "undefined" : false;
        } catch (e) {
            isNodeWebkit = false;
        }

        if (isNodeWebkit) {
            // node-webkitで動作
            return true;
        } else if (isNode) {
            // Node.js上で動作している
            return true;
        } else {
            //  通常のWebページとして動作している
            return false;
        }
    }),
        ($.isNeedClickAudio = function () {
            //プレイヤーはクリックの必要なし
            if ($.isTyranoPlayer()) {
                return false;
            }

            //ブラウザやスマホアプリは必要
            if ($.isElectron() || $.isNWJS()) {
                return false;
            }

            return true;
        });

    ($.isElectron = function () {
        if (navigator.userAgent.indexOf("TyranoErectron") != -1) {
            return true;
        } else {
            return false;
        }
    }),
        //オブジェクトを引き継ぐ。
        ($.extendParam = function (pm, target) {
            var tmp = target;

            for (key in target) {
                if (pm[key]) {
                    if (pm[key] != "") {
                        target[key] = pm[key];
                    }
                }
            }

            return target;
        });

    ($.insertRule = function (css_str) {
        var sheet = (function () {
            var style = document.createElement("style");
            document.getElementsByTagName("head")[0].appendChild(style);
            return style.sheet;
        })();
        sheet.insertRule(css_str, 0);
    }),
        ($.swfName = function (str) {
            if (navigator.appName.indexOf("Microsoft") != -1) {
                return window[str];
            } else {
                return document[str];
            }
        });

    //古いトランス。
    $.trans_old = function (method, j_obj, time, mode, callback) {
        if (method == "crossfade" || mode == "show") {
            if (time == 0) {
                if (mode == "show") {
                    j_obj.show();
                } else {
                    j_obj.hide();
                }
                if (callback) {
                    callback();
                }
            } else {
                var ta = {};

                if (mode == "show") {
                    ta = {
                        opacity: "show",
                    };
                } else {
                    ta = {
                        opacity: "hide",
                    };
                }

                j_obj.animate(ta, {
                    duration: time,
                    easing: "linear",
                    complete: function () {
                        if (callback) {
                            callback();
                        }
                    }, //end complerte
                });
            }

            return false;
        } else {
            if (mode == "hide") {
                j_obj.hide(method, time, function () {
                    if (callback) callback();
                });
            } else if (mode == "show") {
                j_obj.show(method, time, function () {
                    if (callback) callback();
                });
            }
        }
    };

    //コンバート v450rc5以前
    var _map_conv_method = {
        corssfade: "fadeIn",
        explode: "zoomIn",
        slide: "slideInLeft",
        blind: "bounceIn",
        bounce: "bounceIn",
        clip: "flipInX",
        drop: "slideInLeft",
        fold: "fadeIn",
        puff: "fadeIn",
        scale: "zoomIn",
        shake: "fadeIn",
        size: "zoomIn",
    };

    $.trans = function (method, j_obj, time, mode, callback) {
        if (method == "crossfade") {
            method = "fadeIn";
        } else if (_map_conv_method[method]) {
            method = _map_conv_method[method];
        }

        j_obj.css("animation-duration", parseInt(time) + "ms");

        if (mode == "hide") {
            j_obj.show();
            method = $.replaceAll(method, "In", "Out");
            var animationEnd = "webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend";
            j_obj.addClass("animated " + method).one(animationEnd, function () {
                j_obj.off(animationEnd);
                j_obj.css("animation-duration", "");
                $(this).remove();
                if (callback) {
                    //callback();
                }
            });
        } else if (mode == "show") {
            j_obj.show();
            var animationEnd = "webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend";
            j_obj.addClass("animated " + method).one(animationEnd, function () {
                j_obj.off(animationEnd);
                j_obj.css("animation-duration", "");
                $(this).removeClass("animated " + method);
                if (callback) {
                    callback();
                }
            });
        }
    };

    //要素から空白のオブジェクトを削除して返却する
    $.minifyObject = function (obj) {
        for (key in obj) {
            if (obj[key] == null || obj[key] == "") {
                delete obj[key];
            }
        }

        return obj;
    };

    $.preloadImgCallback = function (j_menu, cb, that) {
        var img_storage = [];

        j_menu.find("img").each(function () {
            img_storage.push($(this).attr("src"));
        });

        //ロードが全て完了したら、ふわっと出す
        var sum = 0;
        for (var i = 0; i < img_storage.length; i++) {
            that.kag.preload(img_storage[i], function () {
                sum++;
                if (img_storage.length == sum) {
                    cb();
                }
            });
        }

        if (img_storage.length == 0) {
            cb();
        }
    };

    $.setStorage = function (key, val, type) {
        if (type == "webstorage_compress") {
            $.setStorageCompress(key, val);
        } else if (type == "file") {
            $.setStorageFile(key, val);
        } else {
            $.setStorageWeb(key, val);
        }
    };

    //PC版のみ。実行フォルダを取得
    /*
    $.getProcessPath = function(){
        var path = process.execPath;
        var tmp_index = path.indexOf(".app");
        var os = "mac";
        if(tmp_index == -1){
            tmp_index = path.indexOf(".exe");
            os="win";
        }
        var tmp_path =  path.substr(0,tmp_index);
        var path_index =0;
        if(os=="mac"){
            path_index = tmp_path.lastIndexOf("/");
        }else{
            path_index = tmp_path.lastIndexOf("\\");
        }

        var out_path = path.substr(0,path_index);
        return out_path;

    };
    */

    $.getOS = function () {
        if ($.isElectron()) {
            if (process.platform == "darwin") {
                return "mac";
            } else {
                return "win";
            }
        } else {
            const ua = window.navigator.userAgent.toLowerCase();
            if (ua.includes("windows nt")) {
                return "win";
            } else if (ua.includes("android")) {
                return "android";
            } else if (ua.includes("iphone") || ua.includes("ipad")) {
                return "ios";
            } else if (ua.includes("mac os x")) {
                return "mac";
            } else {
                return "";
            }
        }
    };

    $.makeSaveKey = function () {
        var S = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        var N = 16;
        let key = Array.from(Array(N))
            .map(() => S[Math.floor(Math.random() * S.length)])
            .join("");
        return key;
    };

    $.getStorage = function (key, type) {
        var gv = "null";

        if (type == "webstorage_compress") {
            gv = $.getStorageCompress(key);
        } else if (type == "file") {
            gv = $.getStorageFile(key);
        } else {
            gv = $.getStorageWeb(key);
        }

        return gv;
    };

    $.setStorageWeb = function (key, val) {
        val = JSON.stringify(val);
        //localStorage.setItem(key, LZString.compress(escape(val)));
        try {
            localStorage.setItem(key, escape(val));
        } catch (e) {
            console.error("セーブデータ。localstorageが利用できません。");
            return;
        }
    };

    $.getStorageWeb = function (key) {
        try {
            var gv = "null";

            if (localStorage.getItem(key)) {
                //gv = unescape(LZString.decompress(localStorage.getItem(key)));
                gv = unescape(localStorage.getItem(key));
            }

            if (gv == "null") return null;
        } catch (e) {
            //alert("この環境はセーブ機能を利用できません。ローカルで実行している場合などに発生します");
            //$.confirmSaveClear();

            console.error("セーブデータ。localstorageが利用できません。");
            return null;
        }

        return gv;
    };

    $.playerHtmlPath = function (html) {
        if ("appJsInterface" in window) {
            //Android
        } else {
            if (typeof TyranoPlayer == "function") {
                //playerの場合HTMLを修正する必要がある
                var result_html = "";
                while (1) {
                    var index = html.indexOf("file:///");
                    if (index == -1) {
                        result_html += html;
                        break;
                    } else {
                        result_html += html.substring(0, index);
                        html = html.substring(index, html.length);

                        var replace_index = html.indexOf("/game/data");
                        tmp_html = html.substring(replace_index + "/game/data".length, html.length);
                        html = "./data" + tmp_html;
                    }
                }

                if (result_html != "") {
                    html = result_html;
                }
            }
        }

        return html;
    };

    $.confirmSaveClear = function () {
        if (confirm("セーブデータが壊れている可能性があります。セーブデータを初期化しますか？")) {
            alert("初期化");
            localStorage.clear();
        }
    };

    $.setStorageCompress = function (key, val) {
        val = JSON.stringify(val);
        localStorage.setItem(key, LZString.compress(escape(val)));
        //localStorage.setItem(key, escape(val));
    };

    $.getStorageCompress = function (key) {
        try {
            var gv = "null";

            if (localStorage.getItem(key)) {
                gv = unescape(LZString.decompress(localStorage.getItem(key)));

                if (gv == "null") {
                    gv = unescape(localStorage.getItem(key));
                }
            }

            if (gv == "null") return null;
        } catch (e) {
            console.log("==============");
            console.log(e);
            alert("この環境はセーブ機能を利用できません。ローカルで実行している場合などに発生します");
            $.confirmSaveClear();
        }

        return gv;
    };

    $.getExtWithFile = function (str) {
        var filename = "";
        if (str.indexOf("/") != -1) {
            filename = str.split("/").pop();
        } else {
            filename = str;
        }

        var dir_name = $.replaceAll(str, filename, "");

        var ext = "";
        if (filename.indexOf(".") != -1) {
            ext = str.split(".").pop();
        } else {
            ext = "";
            //拡張子がない場合はディレクトリ名とする。
            dir_name = str;
        }
        var name = $.replaceAll(filename, "." + ext, "");

        return { filename: filename, ext: ext, name: name, dir_name: dir_name };
    };

    //PC用の実行パスを取得
    $.getExePath = function () {
        const _app = require("electron").remote.app;

        //TyranoStudio.app/Contents/Resources/app
        let path = _app.getAppPath();
        let platform = "";
        //alert(process.platform);
        //console.log(process.platform)
        //console.log(path);

        if (process.platform == "darwin") {
            platrofm = "mac";
            //TyranoStudio-darwin-x64.asar
            if (path.indexOf(".asar") != -1) {
                path = $.replaceAll(path, "/Contents/Resources/app.asar", "");
            } else {
                path = $.replaceAll(path, "/Contents/Resources/app", "");
            }

            path = $.getExtWithFile(path).dir_name;
        } else if (process.platform == "win32") {
            if (path.indexOf(".asar") != -1) {
                path = $.replaceAll(path, "\\resources\\app.asar", "");
            } else {
                path = $.replaceAll(path, "\\resources\\app", "");
            }
        }

        return path;
    };

    //展開先のパスを返す。
    $.getUnzipPath = function () {
        let path = __dirname;

        if (path.indexOf(".asar") != -1) {
            return "asar";
        }

        return path;
    };

    $.setStorageFile = function (key, val) {
        val = JSON.stringify(val);
        var fs = require("fs");

        var out_path = $.getExePath();

        //mac os Sierra 対応
        if (process.execPath.indexOf("var/folders") != -1) {
            out_path = process.env.HOME + "/_TyranoGameData";
            if (!fs.existsSync(out_path)) {
                fs.mkdirSync(out_path);
            }
        } else {
            out_path = $.getExePath();
        }

        fs.writeFileSync(out_path + "/" + key + ".sav", escape(val));
    };

    $.getStorageFile = function (key) {
        try {
            var gv = "null";
            var fs = require("fs");
            var out_path = "";

            var out_path = $.getExePath();

            if (process.execPath.indexOf("var/folders") != -1) {
                out_path = process.env.HOME + "/_TyranoGameData";
                if (!fs.existsSync(out_path)) {
                    fs.mkdirSync(out_path);
                }
            } else {
                out_path = $.getExePath();
            }

            if (fs.existsSync(out_path + "/" + key + ".sav")) {
                var str = fs.readFileSync(out_path + "/" + key + ".sav");
                gv = unescape(str);
            } else {
                //Fileが存在しない場合にローカルストレージから読み取る使用は破棄。
                //gv = unescape(localStorage.getItem(key));
            }

            if (gv == "null") {
                return null;
            }
        } catch (e) {
            console.log(e);
            alert("この環境はセーブ機能を利用できません。ローカルで実行している場合などに発生します");
            $.confirmSaveClear();
        }

        return gv;
    };

    /*
    $.setStorage = function(key,val){
    val = JSON.stringify(val);
    localStorage.setItem(key, LZString.compress(escape(val)));
    //localStorage.setItem(key, escape(val));
    };

    $.getStorage = function(key){

    try{

    var gv = "null";

    if(localStorage.getItem(key)){
    gv = unescape(LZString.decompress(localStorage.getItem(key)));

    if(gv=="null"){
    gv = unescape(localStorage.getItem(key));
    }
    }

    if(gv =="null") return null;

    }catch(e){
    alert("この環境はセーブ機能を利用できません。ローカルで実行している場合などに発生します");
    }

    return gv;

    };
    */

    $.alert = function (str, cb) {
        $(".remodal_title").html(str);

        $(".remodal").find(".remodal-cancel").hide();
        $(".remodal").find(".remodal-confirm").show();

        var inst = $("[data-remodal-id=modal]").remodal();
        inst.open();

        $(document).off("closed", ".remodal");
        $(document).on("closed", ".remodal", function (e) {
            if (typeof cb == "function") {
                cb();
            }
        });

        /*
        if ($.userenv() != "pc") {
            alert(str);
            if(typeof cb == "function"){
                cb();
            }

        }else{
            alertify.alert(str,function(){;
                if(typeof cb == "function"){
                    cb();
                }
            });
        }
        */
    };

    $.inform = function (str, type) {
        alertify.log(str, type);
    };

    $.confirm = function (str, cb_ok, cb_cancel) {
        $(".remodal_title").html(str);

        $(".remodal").find(".remodal-cancel").show();
        $(".remodal").find(".remodal-confirm").show();

        var inst = $("[data-remodal-id=modal]").remodal();
        inst.open();

        /////////OK /////////////

        $(document).off("closed", ".remodal");

        $(document).off("confirmation", ".remodal");
        $(document).on("confirmation", ".remodal", function (e) {
            $(document).off("confirmation", ".remodal");
            $(document).off("cancellation", ".remodal");

            if (typeof cb_ok == "function") {
                cb_ok();
            }
        });

        ///////キャンセル//////////////
        $(document).off("cancellation", ".remodal");
        $(document).on("cancellation", ".remodal", function (e) {
            $(document).off("confirmation", ".remodal");
            $(document).off("cancellation", ".remodal");

            if (typeof cb_cancel == "function") {
                cb_cancel();
            }
        });

        /*
        if ($.userenv() != "pc") {

            if(window.confirm(str)){
                cb_ok();
            }else{
                cb_cancel();
            }

        }else{
            alertify.confirm(str,function(e){
                if (e) {
                // user clicked "ok"
                    cb_ok();
                } else {
                    // user clicked "cancel"
                    cb_cancel();
                }
            });
        }
        */
    };

    //オブジェクトの個数をもってきます。1
    $.countObj = function (obj) {
        var num = 0;
        for (key in obj) {
            num++;
        }
        return num;
    };

    $.getUrlQuery = function (url) {
        var hash = url.slice(1).split("&");
        var max = hash.length;
        var vars = {};
        var array = "";

        for (var i = 0; i < max; i++) {
            array = hash[i].split("=");
            vars[array[0]] = array[1];
        }

        return vars;
    };

    //アトリビュートの中で保存するリストを取得する
    $.makeSaveJSON = function (el, array_white_attr) {
        var j_el = $($.playerHtmlPath($(el).outerHTML()));

        var root = {
            tag: el.tagName,
            style: j_el.attr("style"),
            class: j_el.attr("class"),
            text: "",
            attr: {},
            children: [],
        };

        //属性を設置
        for (var k = 0; k < array_white_attr.length; k++) {
            if (j_el.attr(array_white_attr[k])) {
                root["attr"][array_white_attr[k]] = j_el.attr(array_white_attr[k]);
            }
        }

        loop(el, root);

        function loop(node, _root) {
            var nodes = node.childNodes;
            for (var i = 0; i < nodes.length; i++) {
                //console.log(nodes[i]);
                var j_node = $(nodes[i]);

                var obj = {
                    tag: nodes[i].tagName,
                    style: j_node.attr("style"),
                    class: j_node.attr("class"),
                    text: j_node.text(),
                    attr: {},
                    children: [],
                };

                //属性を設置
                for (var k = 0; k < array_white_attr.length; k++) {
                    if (j_node.attr(array_white_attr[k])) {
                        obj["attr"][array_white_attr[k]] = j_node.attr(array_white_attr[k]);
                    }
                }

                if (!nodes[i]) {
                    console.error("セーブデータ作成エラー");
                    console.error(nodes[i]);
                    continue;
                }

                if (nodes[i].childNodes.length > 0) {
                    loop(nodes[i], obj);
                }

                _root.children.push(obj);
            }
        }

        return root;
    };

    //レイヤーをhtmlエレメントとして復元する
    $.makeElementFromSave = function (root, array_white_attr) {
        if (root.tag.toLowerCase() == "script") return false;

        var j_root = $(document.createElement(root.tag));

        j_root.attr("style", root["style"]);
        j_root.attr("class", root["class"]);

        for (var i = 0; i < array_white_attr.length; i++) {
            if (typeof root["attr"][array_white_attr[i]] != "undefined") {
                j_root.attr(array_white_attr[i], root["attr"][array_white_attr[i]]);
            }
        }

        loop(root, j_root);

        function loop(_root, _j_obj) {
            var nodes = _root.children;
            for (var i = 0; i < nodes.length; i++) {
                if (typeof nodes[i].tag != "undefined" && nodes[i].tag.toLowerCase() == "script") {
                    break;
                    return false;
                }

                if (typeof nodes[i].tag == "undefined") {
                    _j_obj.append(nodes[i].text);
                    continue;
                }

                var j_node = $(document.createElement(nodes[i].tag));

                j_node.attr("style", nodes[i]["style"]);
                j_node.attr("class", nodes[i]["class"]);

                for (var k = 0; k < array_white_attr.length; k++) {
                    if (typeof nodes[i]["attr"][array_white_attr[k]] != "undefined") {
                        j_node.attr(array_white_attr[k], nodes[i]["attr"][array_white_attr[k]]);
                    }
                }

                if (!nodes[i]) {
                    console.error("セーブデータ作成エラー");
                    console.error(nodes[i]);
                    continue;
                }

                if (nodes[i].children.length > 0) {
                    loop(nodes[i], j_node);
                }

                _j_obj.append(j_node);
            }
        }

        return j_root;
    };

    //渡されたJqueryオブジェクトにクラスをセットします
    $.setName = function (jobj, str) {
        str = $.trim(str);

        if (str == "") return;

        var array = str.split(",");
        for (var i = 0; i < array.length; i++) {
            jobj.addClass(array[i]);
        }
    };

    //フラッシュのインストール判定
    $.isFlashInstalled = function () {
        if (navigator.plugins["Shockwave Flash"]) {
            return true;
        }
        try {
            new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
            return true;
        } catch (e) {
            return false;
        }
    };

    /*スマホの場合は、タッチでクリックを置き換える*/
    /*タッチ系、一応出来たけど、動作確認よくしなければならなｋ，問題なければR9にも適応*/
    if ($.userenv() != "pc") {
        $.event.tap = function (o) {
            o.bind("touchstart", onTouchStart_);
            function onTouchStart_(e) {
                e.preventDefault();
                o.data("event.tap.moved", false).one("touchmove", onTouchMove_).one("touchend", onTouchEnd_);
                e.stopPropagation();
            }

            function onTouchMove_(e) {
                //o.data('event.tap.moved', true);
                e.stopPropagation();
            }

            function onTouchEnd_(e) {
                if (!o.data("event.tap.moved")) {
                    o.unbind("touchmove", onTouchMove_);
                    o.trigger("click").click();
                    e.stopPropagation();
                }
            }
        };

        if ("ontouchend" in document) {
            $.fn.tap = function (data, fn) {
                //alert("tap!");

                if (fn == null) {
                    fn = data;
                    data = null;
                }

                if (arguments.length > 0) {
                    this.bind("tap", data, fn);
                    $.event.tap(this);
                } else {
                    this.trigger("tap");
                }
                return this;
            };

            if ($.attrFn) {
                $.attrFn["tap"] = true;
            }

            //クリック上書き
            $.fn.click = $.fn.tap;
        } else {
            //$.fn.tap = $.fn.click;
        }
    }

    //////////////////////////////

    $.error_message = function (str) {
        alert(str);
    };

    //クッキー設定
    $.setCookie = function (key, val) {
        document.cookie = key + "=" + escape(val) + ";expires=Fri, 31-Dec-2030 23:59:59;path=/;";
    };

    // window.setTimeoutのラッパー関数
    // timeoutが0より大きい数値ならwindow.setTimeoutに投げて非同期実行（戻り値はtimerId:正の整数）
    // そうでないならcallbackを同期実行（戻り値は0）
    // ※window.setTimeout(callback, 0)は非同期実行になってしまう
    $.setTimeout = function (callback, timeout) {
        if (typeof timeout === "number" && timeout > 0) {
            return setTimeout(callback, timeout);
        }
        callback();
        return 0;
    };

    /**
     * @typedef {Object} EdgeOption
     * @property {string} color
     * @property {number} width
     * @property {number} total_width
     */
    /**
     * 縁取りの太さと幅を指定した文字列を解析してEdgeの配列を返す
     * たとえば $.parseEdgeOptions("4px rgb(255,0,0), 2px 0xFFFFFF, blue") は次の配列を返す
     * [
     *   {color: "rgb(255,0,0)", width: 4, total_width: 4},
     *   {color: "#FFFFFF", width: 2, total_width: 6},
     *   {color: "blue", width: 1, total_width: 7}
     * ]
     * @param {string} edge_str 縁取りの太さと幅 (例) "4px rgb(255,0,0), 2px 0xFFFFFF"
     * @returns {EdgeOption[]}
     */
    $.parseEdgeOptions = function (edge_str) {
        // キャッシュを活用
        const cache_map = $.parseEdgeOptions.cache;
        if (edge_str in cache_map) {
            return cache_map[edge_str];
        }

        // 戻り値となるEdgeの配列
        const edges = [];

        // 文字列を「カッコの外にあるカンマ」で刻む
        // 色指定自体にカンマが含まれるケース（"rgb(255,255,255)"のような）を考慮しなければならない
        const edge_str_hash = edge_str.split(/,(?![^\(]*\))/);

        // 内側から加算していった合計の縁取り太さ（複数縁取りを行う場合に意味を持つ）
        // filter: drop-shadow()方式では不要、text-shadow方式や-webkit-text-stroke方式では必要
        let total_width = 0;

        for (const this_edge_str of edge_str_hash) {
            // 例) "6px Black"
            const this_edge_str_trim = $.trim(this_edge_str);

            // 先頭の〇〇pxをチェック
            const width_match = this_edge_str_trim.match(/^[0-9\.]+px /);

            let width;
            let width_str;
            if (width_match) {
                // 先頭の〇〇pxが見つかればそれを縁取りの太さとして解釈し、〇〇pxよりもあとの文字列を色解析に回す
                width = parseFloat(width_match[0]);
                width_str = this_edge_str_trim.substring(width_match[0].length);
            } else {
                // 先頭の〇〇pxが見つからなければ太さは1とし、文字列をまるごと色解析に回す
                width = 1;
                width_str = this_edge_str_trim;
            }

            const color = $.convertColor($.trim(width_str));
            total_width += width;
            if (color) {
                edges.push({ color, width, total_width });
            }
        }
        cache_map[edge_str] = edges;
        return edges;
    };
    $.parseEdgeOptions.cache = {};

    /**
     * 縁取りしたいDOM要素のスタイルのfilterプロパティにセットするべき値を生成する
     * @param {string} edge_str 縁取りの太さと幅 (例) "4px red, 2px white"
     * @returns {string} (例) "drop-shadow(0 0 4px red) drop-shadow(0 0 red) ..."
     */
    $.generateDropShadowStrokeCSS = function (edge_str) {
        // 毎回計算するのは意外と重いのでキャッシュを活用
        const cache_map = $.generateDropShadowStrokeCSS.cache;
        if (edge_str in cache_map) {
            return cache_map[edge_str];
        }

        // "drop-shadow(...)" を格納していく配列
        const css_arr = [];

        const edges = $.parseEdgeOptions(edge_str);
        for (const edge of edges) {
            css_arr.push($.generateDropShadowStrokeCSSOne(edge.color, edge.width));
        }

        const css_value = css_arr.join(" ");
        cache_map[edge_str] = css_value;
        return css_value;
    };
    $.generateDropShadowStrokeCSS.cache = {};

    /**
     * 縁取りしたいDOM要素のスタイルのfilterプロパティにセットするべき値を生成する
     * @param {string} color 縁取りの色 (例) "red"
     * @param {number} width 縁取りの幅 (例) 3
     * @returns {string} 例) "drop-shadow(0 0 3px red) drop-shadow(0 0 red) ..."
     */
    $.generateDropShadowStrokeCSSOne = function (color = "black", width = 1) {
        // drop-shadow(...)を重ねる
        // 試行錯誤の結果これが良い感じと思われた
        const shadow_width = (width - 1) * 0.4;
        const css_array = [];
        if (shadow_width > 0) {
            css_array.push(`drop-shadow(0 0 ${shadow_width.toFixed(2)}px ${color})`);
            for (let i = 0; i < 8; i++) {
                css_array.push(`drop-shadow(0 0 ${color})`);
            }
        }
        // 最後にうっすらとぼかした細い影を落とすことでアンチエイリアス効果を与える
        // これがないと縁取りがガビガビに見えてしまう
        css_array.push(`drop-shadow(0 0 0.4px ${color})`);
        css_array.push(`drop-shadow(0 0 0.4px ${color})`);
        css_array.push(`drop-shadow(0 0 0.2px ${color})`);
        return css_array.join(" ");
    };

    if ($.getOS() === "ios" && $.getBrowser() === "safari") {
        $.generateDropShadowStrokeCSSOne = function (color = "black", width = 1) {
            const shadow_width = Math.max(1, parseInt(width * 0.5));
            console.warn(shadow_width);
            const css_array = [];
            if (shadow_width > 0) {
                css_array.push(`drop-shadow(0 0 ${shadow_width}px ${color})`);
                for (let i = 0; i < 8; i++) {
                    css_array.push(`drop-shadow(0 0 ${color})`);
                }
            }
            return css_array.join(" ");
        };
    }

    /**
     * 縁取りしたいDOM要素のスタイルのtext-shadowプロパティにセットするべき値を生成する
     * @param {string} edge_str 縁取りの太さと幅 (例) "4px red, 2px white"
     * @returns {string} 例) "1px 1px 0px red, -1px 1px 0px red, ..."
     */
    $.generateTextShadowStrokeCSS = function (edge_str) {
        // 毎回計算するのは意外と重いのでキャッシュを活用
        const cache_map = $.generateTextShadowStrokeCSS.cache;
        if (edge_str in cache_map) {
            return cache_map[edge_str];
        }

        // "1px 1px 0px black" のような文字列を格納していく配列
        const css_arr = [];

        const edges = $.parseEdgeOptions(edge_str);
        for (const edge of edges) {
            css_arr.push($.generateTextShadowStrokeCSSOne(edge.color, edge.total_width));
        }

        const css_value = css_arr.join(",");
        cache_map[edge_str] = css_value;
        return css_value;
    };
    $.generateTextShadowStrokeCSS.cache = {};

    /**
     * 縁取りしたいDOM要素のスタイルのtext-shadowプロパティにセットするべき値を生成する
     * @param {string} color 縁取りの色 例) "black"
     * @param {number} width 縁取りの幅 例) 3
     * @returns {string} 例) "1px 1px 0px black, -1px 1px 0px black, ..."
     */
    $.generateTextShadowStrokeCSSOne = function (color = "black", width = 1) {
        // 円周上の頂点を取得
        const points = $.calcTextShadowStrokePoints(width);

        // 座標の小数点以下の桁数
        const position_digits = 2;

        // text-shadowを重ねる
        const css_array = [];
        for (let p of points) {
            const x = p.x.toFixed(position_digits);
            const y = p.y.toFixed(position_digits);
            const css = `${x}px ${y}px 0px ${color}`;
            css_array.push(css);
        }

        return css_array.join(",");
    };

    /**
     * text-shadowで文字の縁取りを行うときの、陰をずらす先の頂点の座標配列を計算する
     * 太い縁取りを行う場合ほど大量の頂点を生み出す必要がある
     * @param {number} width 縁取りの幅 (例) 3
     * @returns {{x: number; y: number;}[]}
     */
    $.calcTextShadowStrokePoints = function (width) {
        // 太さが1以下の場合は固定値を返す
        if (width <= 1) {
            return [
                { x: 1, y: -1 },
                { x: 1, y: 1 },
                { x: -1, y: 1 },
                { x: -1, y: -1 },
            ];
        }

        // 円周の長さ
        const circumference = 2 * width * Math.PI;

        // 円周をこの長さで分割する
        const hash_length = 1;

        // 頂点の数の最低値
        const point_num_min = 8;

        // 頂点の数（円周の長さ÷分割する長さ）
        const point_num = Math.max(point_num_min, circumference / hash_length);

        // 1周（2πラジアン＝360°）
        const round = 2 * Math.PI;

        // 1周をこの角度で分割する
        const hash_angle = round / point_num;

        // 円周上の点の座標を格納する配列
        const points = [];

        for (let angle = 0; angle < round; angle += hash_angle) {
            points.push({
                x: width * Math.cos(angle),
                y: width * Math.sin(angle),
            });
        }

        return points;
    };

    /**
     * CSSのfilterプロパティに値をセット
     * prefixを考慮
     * @param {string} str filterプロパティに値をセット
     * @return {jQuery}
     */
    $.fn.setFilterCSS = function (str) {
        // transform: translateZ(0); でGPUレイヤー作成を促す
        // Safari on iOS においてfilterプロパティだけではGPUレイヤーが作成されず
        // filterが崩れる可能性がある
        return this.setStyleMap({
            "-webkit-filter": str,
            "-ms-filter": str,
            "-moz-filter": str,
            "transform": "translateZ(0)",
        });
    };

    /**
     * CSSグラデーションのプリセット
     */
    $.gradientPresetMap = {
        dark: "linear-gradient(0deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 41%, rgba(126,126,126,1) 100%)",
        light: "linear-gradient(0deg, rgba(193,245,239,1) 0%, rgba(255,255,255,1) 34%, rgba(255,255,255,1) 100%)",
        fire: "linear-gradient(0deg, rgba(255,0,0,1) 0%, rgba(255,239,0,1) 100%)",
        sky: "linear-gradient(0deg, rgba(0,255,235,1) 0%, rgba(0,18,255,1) 100%)",
        leaf: "linear-gradient(0deg, rgba(234,240,0,1) 0%, rgba(0,226,49,1) 100%)",
        gold: "repeating-linear-gradient(0deg, #B67B03 0.1em, #DAAF08 0.2em, #FEE9A0 0.3em, #DAAF08 0.4em, #B67B03 0.5em)",
        gold2: "linear-gradient(0deg, #b8751e 0%, #ffce08 37%, #fefeb2 47%, #fafad6 50%, #fefeb2 53%, #e1ce08 63%, #b8751e 100%)",
        silver: "repeating-linear-gradient(0deg, #a8c7c3 0.1em, #b6c9d1 0.2em, #e7fbff 0.3em, #c7d5d6 0.4em, #a6b2b6 0.5em)",
        silver2: "linear-gradient(0deg, #acb4b8 0%, #e6f8ff 37%, #e6f7f8 47%, #e2f3fd 50%, #eff9ff 53%, #d8e4e7 63%, #b5bbbd 100%)",
    };

    /**
     * グラデーションテキストを設定する
     * @param {string} gradient CSSのグラデーション関数文字列 linear-gradient(...)
     * @return {jQuery}
     */
    $.fn.setGradientText = function (gradient) {
        if (this.length === 0) {
            return this;
        }
        if (gradient in $.gradientPresetMap) {
            gradient = $.gradientPresetMap[gradient];
        }
        this.each(function () {
            $(this)
                .setStyleMap({
                    "background-image": gradient,
                    "-webkit-background-clip": "text",
                    "background-clip": "text",
                    "color": "transparent",
                })
                .addClass("gradient-text");
        });
        return this;
    };

    /**
     * グラデーションテキストを復元する
     * @return {jQuery}
     */
    $.fn.restoreGradientText = function () {
        if (this.length === 0) {
            return this;
        }
        this.each(function () {
            const j_this = $(this);
            const style = j_this.attr("style");
            if (style && !style.includes("-webkit-background-clip") && style.includes("background-clip")) {
                const new_style = style.replace("background-clip", "-webkit-background-clip: text; background-clip");
                j_this.attr("style", new_style);
            }
        });
        return this;
    };

    /**
     * -webkit-text-strokeで縁取りされている可能性のある
     * [ptext]のテキスト内容を書き換える
     * @param {string} str 新しいテキスト
     * @return {jQuery}
     */
    $.fn.updatePText = function (str) {
        if (this.length === 0) {
            return this;
        }
        this.each(function () {
            if (typeof this.updateText === "function") {
                this.updateText(str);
            } else {
                $(this).html(str);
            }
        });
        return this;
    };

    /**
     * 引数が"空でない文字列"であるかどうかを返す
     * @param {any} val
     * @returns {boolean}
     */
    $.isNonEmptyStr = function (val) {
        if (typeof val === "string" && val !== "") {
            return true;
        }
        return false;
    };

    /**
     * CSSのオブジェクトを渡してセットする
     * 本家jQueryの.css()メソッドは汎用性が高い分処理が遅い
     * こちらのメソッドであれば処理時間が40-50%ほどで済む
     * @param {Object} map CSSのプロパティと値が対になっているオブジェクト
     * @return {jQuery}
     */
    $.fn.setStyleMap = function (map) {
        const len = this.length;
        if (len === 0) {
            return this;
        }
        for (let i = 0; i < len; i++) {
            const elm = this[i];
            for (const key in map) {
                elm.style.setProperty(key, map[key]);
            }
        }
        return this;
    };

    /**
     * CSSのキーとバリューを渡してセットする
     * 本家jQueryの.css()メソッドは汎用性が高い分処理が遅い
     * こちらのメソッドであれば処理時間が40-50%ほどで済む
     * @param {Object} key
     * @param {Object} value
     * @return {jQuery}
     */
    $.fn.setStyle = function (key, value) {
        const len = this.length;
        if (len === 0) {
            return this;
        }
        for (let i = 0; i < len; i++) {
            const elm = this[i];
            elm.style.setProperty(key, value);
        }
        return this;
    };

    /**
     * 要素に直接指定されているスタイルの値を取得する
     * なにも指定されていない場合は空の文字列が帰る
     * ※ .css()とは違う。.css()は.getComputedStyle()がベースになっている。
     * 　 .css()は、要素自体には何のスタイルも指定されていない場合でも、読み込まれているCSSを考慮して
     * 　 最終的にどのようなスタイルが当たるのかを判断し、さらに長さのプロパティをpxに変換して返す仕様がある。
     * ※ ここで定義している.getStyle()は単純に『この要素に直接』指定されているスタイルを返す。
     * @param {string | string[]} prop
     * @return {string}
     */
    $.fn.getStyle = function (prop) {
        if (this[0]) {
            if (typeof prop === "string") {
                return this[0].style.getPropertyValue(prop);
            } else {
                const style_map = {};
                prop.forEach((this_prop) => {
                    style_map[this_prop] = this[0].style.getPropertyValue(this_prop);
                });
                return style_map;
            }
        }
        return "";
    };

    /**
     * 渡されたjQueryコレクション内のすべての要素について横幅を調査し、
     * その調査で得られたもっとも大きい横幅をすべての要素のwidthプロパティにpx単位で適用する
     * box-sizingも考慮する
     * @returns {jQuery}
     */
    $.fn.alignMaxWidth = function () {
        return this.alignMaxWidthOrHeight("width", "left", "right");
    };
    $.fn.alignMaxHeight = function () {
        return this.alignMaxWidthOrHeight("height", "top", "bottom");
    };
    $.fn.alignMaxWidthOrHeight = function (_width, _left, _right) {
        const len = this.length;
        if (len === 0) {
            return this;
        }
        let max_width = -1;
        let j_max_elm;
        this.each((i, elm) => {
            const j_elm = $(elm);
            // border-box にしておく → 横幅を統一的に解釈するため
            // display: block にしておく → 表示状態でないと横幅が取得できないため
            j_elm.setStyle("box-sizing", "border-box").show();
            const computed_style = j_elm.css([
                "box-sizing",
                `padding-${_left}`,
                `padding-${_right}`,
                `border-${_left}-width`,
                `border-${_right}-width`,
            ]);
            const padding_sum =
                parseFloat(computed_style[`padding-${_left}`]) +
                parseFloat(computed_style[`padding-${_right}`]) +
                parseFloat(computed_style[`border-${_left}-width`]) +
                parseFloat(computed_style[`border-${_right}-width`]);
            const client_width = j_elm[_width]() + padding_sum;
            if (client_width > max_width) {
                max_width = client_width;
                j_max_elm = j_elm;
            }
        });
        const width = j_max_elm.getStyle("width");
        this.setStyle(_width, `${max_width}px`);
        return this;
    };

    /**
     * ティラノスクリプトの[kanim]に渡されたパラメータを用いて
     * 任意のDOM要素にWeb Animation APIによるキーフレームアニメーションを適用する
     * @param {Object} pm
     * @return {jQuery}
     */
    $.fn.animateWithTyranoKeyframes = function (pm) {
        const len = this.length;
        if (len === 0) {
            return this;
        }
        const keyframes = TYRANO.kag.parseKeyframesForWebAnimationAPI(pm.keyframe);
        if (!keyframes) {
            return this;
        }
        for (let i = 0; i < len; i++) {
            const anim = this[i].animate(keyframes, {
                delay: parseInt(pm.delay) || 0,
                direction: pm.direction || "normal",
                duration: parseInt(pm.time) || 1000,
                easing: pm.easing || "linear",
                iterations: pm.count === "infinite" ? Infinity : parseInt(pm.count) || Infinity,
                fill: pm.mode || "forwards",
            });
            anim.onfinish = () => {
                if (pm.onend) {
                    pm.onend(anim);
                }
            };
        }
        return this;
    };

    /**
     * dataフォルダに入っていることが想定されるフォルダ名("scenario", "image"など)を格納した配列
     */
    const data_folder_names = ["scenario", "image", "fgimage", "bgimage", "video", "sound", "bgm", "others"];

    /**
     * タグのstorageパラメータに指定された値をフルパスに直す
     * - "http" から始まる場合はそのまま返す。
     * - そうでない場合、戻り値が "./" で始まることを保証する。
     * - 同等のパスが一意に定まるパスで表されることを保証する。
     * - そのために "../" を除去する。
     * - "../" を許可してしまうと、同等のパスを無限のパターンで表せてしまうため、
     *   たとえばパスをキーにした連想配列でキャッシュ管理をしている場合に
     *   キャッシュが機能しないケースが出てきてしまう。
     * @param {string} storage "foo.png"
     * @param {string} folder "image"
     * @returns {string}
     * @example
     * $.parseStorage("foo.png", "image");
     * // "./data/image/foo.png"
     * $.parseStorage("https://tyrano.jp/foo.png", "image");
     * // "https://tyrano.jp/foo.png"
     * $.parseStorage("nextpage.gif", "tyrano/images/system");
     * // "./tyrano/images/system/nextpage.gif"
     * $.parseStorage("foo.png", "data/image");
     * // "./data/image/foo.png"
     * $.parseStorage("../fgimage/foo.png", "image");
     * // "./data/fgimage/foo.png"
     */
    $.parseStorage = function (storage, folder = "") {
        // "http"で始まっているならそのまま返す
        if ($.isHTTP(storage)) {
            return storage;
        }

        // フォルダパスを特定
        if (folder && data_folder_names.includes(folder.split("/").shift())) {
            // dataフォルダに入っているフォルダ名が指定されている場合は自動的に"data/"を足す
            // たとえば"scenario"を"data/scenario"に変換する
            folder = `data/${folder}`;
        }

        // / フォルダパス / ファイル名
        let full_path = `/${folder}/${storage}`;

        // "//" や "/./" は "/" に直す
        full_path = full_path.replace(/\/\.?\/+/g, "/");

        const path_hash = [];

        full_path.split("/").forEach((item) => {
            if (item === "" || item === ".") {
                return;
            }
            if (item === "..") {
                path_hash.pop();
                return;
            }
            path_hash.push(item);
        });

        full_path = "./" + path_hash.join("/");

        return full_path;
    };

    /**
     * "300", "0.3s", "300ms" などでありうる文字列を
     * animation-duration にセットできる値に変換する
     * @param {string} str
     * @returns
     */
    $.convertDuration = function (str, default_value = "0s") {
        if (typeof str !== "string" || str === "") {
            return default_value;
        }
        if (str.includes("s")) {
            return str;
        }
        return str + "ms";
    };

    /**
     * スネークケース(ハイフン区切り)のCSSのプロパティ名を
     * キャメルケースに変換して返す
     * @param {string} str
     * @returns {string}
     * @example
     * $.parseCamelCaseCSS("-webkit-text-stroke");
     * // "webkitTextStroke"
     */
    $.parseCamelCaseCSS = function (str) {
        if (typeof str !== "string") {
            return "";
        }
        // 先頭のハイフンはただ消去するだけでいい
        if (str.charAt(0) === "-") {
            str = str.substring(1);
        }
        // ハイフン＋なんらかの小文字アルファベットのマッチ
        const match = str.match(/\-[a-z]/);
        // マッチしなくなったら完成
        if (!match) {
            return str;
        }
        // マッチし続ける限り再帰する
        return $.parseCamelCaseCSS(str.replace(match[0], match[0].charAt(1).toUpperCase()));
    };

    $.findAnimTargets = function (pm = {}) {
        // アニメーション対象
        let j_target = null;

        if (pm.name) {
            // nameパラメータが指定されている場合
            j_target = $("." + pm.name);
        } else if (pm.layer) {
            // nameパラメータは指定されていないがlayerパラメータが指定されている場合
            // 対象レイヤのクラス名を取得 (例) "layer_free", "0_fore", "1_fore"
            const layer_name = pm.layer == "free" ? "layer_free" : pm.layer + "_fore";
            // レイヤ内の子要素をすべて対象に取る
            j_target = $("." + layer_name).children();
        }

        return j_target || $();
    };

    /**
     * volumeパラメータの値を実際にhowler.jsで利用可能な値に直す
     * "0"～"100" の文字列を 0.0～1.0 の数値に変換する
     * @param {string} vol_str parseInt()で数値に変換可能な文字列 (例) "0", "50", "100"
     * @returns {number} 0以上1以下の数値
     */
    $.parseVolume = function (vol_str) {
        const vol_int = typeof vol_str === "string" ? parseInt(vol_str) : vol_str;
        if (isNaN(vol_int)) {
            return 1;
        }
        return Math.max(0, Math.min(1, vol_str / 100));
    };
})(jQuery);

jQuery.fn.outerHTML = function (s) {
    if (s) {
        this.before(s);
        this.remove();
        return this;
    } else {
        var dummy = jQuery("<p>");
        var elem = this.eq(0);
        dummy.append(elem.clone());
        return dummy.html();
    }
};

// t: current time, b: begInnIng value, c: change In value, d: duration
jQuery.easing["jswing"] = jQuery.easing["swing"];

jQuery.extend(jQuery.easing, {
    def: "easeOutQuad",
    swing: function (x, t, b, c, d) {
        //alert(jQuery.easing.default);
        return jQuery.easing[jQuery.easing.def](x, t, b, c, d);
    },
    easeInQuad: function (x, t, b, c, d) {
        return c * (t /= d) * t + b;
    },
    easeOutQuad: function (x, t, b, c, d) {
        return -c * (t /= d) * (t - 2) + b;
    },
    easeInOutQuad: function (x, t, b, c, d) {
        if ((t /= d / 2) < 1) return (c / 2) * t * t + b;
        return (-c / 2) * (--t * (t - 2) - 1) + b;
    },
    easeInCubic: function (x, t, b, c, d) {
        return c * (t /= d) * t * t + b;
    },
    easeOutCubic: function (x, t, b, c, d) {
        return c * ((t = t / d - 1) * t * t + 1) + b;
    },
    easeInOutCubic: function (x, t, b, c, d) {
        if ((t /= d / 2) < 1) return (c / 2) * t * t * t + b;
        return (c / 2) * ((t -= 2) * t * t + 2) + b;
    },
    easeInQuart: function (x, t, b, c, d) {
        return c * (t /= d) * t * t * t + b;
    },
    easeOutQuart: function (x, t, b, c, d) {
        return -c * ((t = t / d - 1) * t * t * t - 1) + b;
    },
    easeInOutQuart: function (x, t, b, c, d) {
        if ((t /= d / 2) < 1) return (c / 2) * t * t * t * t + b;
        return (-c / 2) * ((t -= 2) * t * t * t - 2) + b;
    },
    easeInQuint: function (x, t, b, c, d) {
        return c * (t /= d) * t * t * t * t + b;
    },
    easeOutQuint: function (x, t, b, c, d) {
        return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
    },
    easeInOutQuint: function (x, t, b, c, d) {
        if ((t /= d / 2) < 1) return (c / 2) * t * t * t * t * t + b;
        return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b;
    },
    easeInSine: function (x, t, b, c, d) {
        return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b;
    },
    easeOutSine: function (x, t, b, c, d) {
        return c * Math.sin((t / d) * (Math.PI / 2)) + b;
    },
    easeInOutSine: function (x, t, b, c, d) {
        return (-c / 2) * (Math.cos((Math.PI * t) / d) - 1) + b;
    },
    easeInExpo: function (x, t, b, c, d) {
        return t == 0 ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
    },
    easeOutExpo: function (x, t, b, c, d) {
        return t == d ? b + c : c * (-Math.pow(2, (-10 * t) / d) + 1) + b;
    },
    easeInOutExpo: function (x, t, b, c, d) {
        if (t == 0) return b;
        if (t == d) return b + c;
        if ((t /= d / 2) < 1) return (c / 2) * Math.pow(2, 10 * (t - 1)) + b;
        return (c / 2) * (-Math.pow(2, -10 * --t) + 2) + b;
    },
    easeInCirc: function (x, t, b, c, d) {
        return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
    },
    easeOutCirc: function (x, t, b, c, d) {
        return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
    },
    easeInOutCirc: function (x, t, b, c, d) {
        if ((t /= d / 2) < 1) return (-c / 2) * (Math.sqrt(1 - t * t) - 1) + b;
        return (c / 2) * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
    },
    easeInElastic: function (x, t, b, c, d) {
        var s = 1.70158;
        var p = 0;
        var a = c;
        if (t == 0) return b;
        if ((t /= d) == 1) return b + c;
        if (!p) p = d * 0.3;
        if (a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else var s = (p / (2 * Math.PI)) * Math.asin(c / a);
        return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b;
    },
    easeOutElastic: function (x, t, b, c, d) {
        var s = 1.70158;
        var p = 0;
        var a = c;
        if (t == 0) return b;
        if ((t /= d) == 1) return b + c;
        if (!p) p = d * 0.3;
        if (a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else var s = (p / (2 * Math.PI)) * Math.asin(c / a);
        return a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + c + b;
    },
    easeInOutElastic: function (x, t, b, c, d) {
        var s = 1.70158;
        var p = 0;
        var a = c;
        if (t == 0) return b;
        if ((t /= d / 2) == 2) return b + c;
        if (!p) p = d * (0.3 * 1.5);
        if (a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else var s = (p / (2 * Math.PI)) * Math.asin(c / a);
        if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b;
        return a * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) * 0.5 + c + b;
    },
    easeInBack: function (x, t, b, c, d, s) {
        if (s == undefined) s = 1.70158;
        return c * (t /= d) * t * ((s + 1) * t - s) + b;
    },
    easeOutBack: function (x, t, b, c, d, s) {
        if (s == undefined) s = 1.70158;
        return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
    },
    easeInOutBack: function (x, t, b, c, d, s) {
        if (s == undefined) s = 1.70158;
        if ((t /= d / 2) < 1) return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
        return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
    },
    easeInBounce: function (x, t, b, c, d) {
        return c - jQuery.easing.easeOutBounce(x, d - t, 0, c, d) + b;
    },
    easeOutBounce: function (x, t, b, c, d) {
        if ((t /= d) < 1 / 2.75) {
            return c * (7.5625 * t * t) + b;
        } else if (t < 2 / 2.75) {
            return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
        } else if (t < 2.5 / 2.75) {
            return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
        } else {
            return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
        }
    },
    easeInOutBounce: function (x, t, b, c, d) {
        if (t < d / 2) return jQuery.easing.easeInBounce(x, t * 2, 0, c, d) * 0.5 + b;
        return jQuery.easing.easeOutBounce(x, t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
    },
});

// windowのloadイベントが発火済みかどうかを管理
window.isLoaded = false;
window.addEventListener("load", function () {
    window.isLoaded = true;
});
