Greasy Fork is available in English.

ピクトセンス - 自動描画[EXT]

自動でキャンバスに画像を描画します。

// ==UserScript==
// @name         ピクトセンス - 自動描画[EXT]
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @license      MIT
// @description  自動でキャンバスに画像を描画します。
// @author       You
// @match        https://pictsense.com/*
// @require      http://code.jquery.com/jquery-3.5.1.min.js
// @require      https://greasyfork.org/scripts/419945-global-managedextensions/code/Global_ManagedExtensions.js?version=889360
// @require      https://greasyfork.org/scripts/419888-antimatterx/code/antimatterx.js?version=889299
// @grant        GM.setValue
// @grant        GM.getValue
// ==/UserScript==

(function(unsafeWindow) {
    'use strict';
    const $ = window.$,
        amx = window.antimatterx;
    let input_colors, input_resolution, monochrome_flag, input_time, loop_max, paintLast_flag, Message, input_file, g_stop_btn_holder, run_flag, g_stop_flag;

    function setConfig() {
        const h = $("<div>"),
            message_holder = $("<div>").appendTo(h);
        Message = function(s) {
            message_holder.text(s);
        };
        g_stop_btn_holder = $("<div>").appendTo(h);
        input_colors = $(amx.addInputRange(h[0], {
            title: "クオリティ",
            width: "40%",
            min: 2,
            max: 256,
            save: "input_colors"
        }));
        input_resolution = $(amx.addInputRange(h[0], {
            title: "解像度",
            width: "40%",
            min: 10,
            max: 550,
            save: "input_resolution"
        }));
        monochrome_flag = $(amx.addInputBool(h[0], {
            title: "モノクロ"
        }));
        reset_input_file();
        amx.addButton(h[0], {
            title: "画像を読み込む",
            click: function() {
                if (run_flag) Message("自動描画中なので読み込めません。");
                else input_file.click();
            }
        });
        paintLast_flag = $(amx.addInputBool(h[0], {
            title: "後ろから塗る"
        }));
        input_time = $(amx.addInputNumber(h[0], {
            title: "描画間隔",
            placeholder: "[秒]",
            value: 1,
            save: "input_time",
            max: 5,
            min: 0
        })).val(1);
        loop_max = $(amx.addInputNumber(h[0], {
            title: "ループ最大数",
            width: "5em",
            value: 3777,
            save: "loop_max",
            max: 3777,
            min: 0
        })).val(3777);
        h.children().each(function(i, e) {
            $(e).after("<br>");
        });
        return h;
    };
    unsafeWindow.Global_ManagedExtensions["自動描画"] = {
        config: setConfig,
        tag: "ピクトセンス"
    };
    const CV_ELM = $("#previewCanvas"); // canvas要素
    let g_W, g_H, // 画像の幅と高さ
        g_pixelClass_Array, // クラスの入れ物
        g_ADD_X, g_ADD_Y,
        g_cv_click_flag = false;

    function reset() {
        run_flag = false;
        reset_input_file();
        g_cv_click_flag = false;
        g_stop_btn_holder.empty();
    };

    function mouse_event(evt_name, x, y, jq) {
        const evt = document.createEvent("MouseEvent");
        evt.initMouseEvent(
            evt_name, // type 設定可能なタイプは click, mosuedown, mouseup, mouseover, mousemove, mouseout
            true, // canBubble bubbleを許可するかどうか
            true, // cancelable 途中で処理を止められるかどうか
            unsafeWindow, // view 処理させるウィンドウのオブジェクト
            1, // detail マウスクリックの回数
            0, // screenX
            0, // screenY
            x - $(window).scrollLeft(), // clientX
            y - $(window).scrollTop(), // clientY
            false, // ctrlKey イベント中にCtrlキーを押した状態にするかどうか
            false, // altKey イベント中にAltキーを押した状態にするかどうか
            false, // shiftKey イベント中にShiftキーを押した状態にするかどうか
            false, // metaKey イベント中にMetaキーを押した状態にするかどうか
            0, // button 0を設定すると左クリック、1で中クリック、2で右クリック
            unsafeWindow // relatedTarget 関連するイベントの設定。MouseOverとMouseOutの時だけ使用するのでそれ以外はnull
        );
        jq.get(0).dispatchEvent(evt);
    };

    function setRGBA(_RGBA) {
        let RGB = "";
        for (let i = 0; i < 3; i++) RGB += ("00" + Number(_RGBA[i]).toString(16)).slice(-2);
        const btn_elm = $("#colorPalette").find("button").eq(0);
        btn_elm.attr("data-color", RGB);
        btn_elm[0].style.backgroundColor = "#" + RGB;
        mouse_event("mousedown", btn_elm.offset().left, btn_elm.offset().top, btn_elm);
        const A = _RGBA[3],
            sld_elm = $("#opacitySlider"),
            Convers = (A / 256) * sld_elm.width();
        mouse_event("mousedown", sld_elm.offset().left + Convers, sld_elm.offset().top, sld_elm);
        mouse_event("mouseup", 0, 0, sld_elm);
        const size_elm = $("#sizeButtonHolder").find("button").eq(0);
        mouse_event("mousedown", size_elm.offset().left, size_elm.offset().top, size_elm);
        mouse_event("mouseup", 0, 0, size_elm);
    };
    class pixelClass {
        constructor(_x, _y, _RGBA) {
            const ar = [];
            const divide = 256 / input_colors.val();
            for (let i = 0; i < 4; i++) ar.push(Math.floor(Math.ceil(_RGBA[i] / divide) * divide));
            this._RGBA = ar.join("_");
            this._flag = ar[0] === 255 && ar[1] === 255 && ar[2] === 255 || ar[3] === 0 ? true : false;
            this.m_y = {
                "isFirst": _y == 0 ? true : false,
                "isEnd": _y == g_H - 1 ? true : false
            };
            this.m_x = {
                "isFirst": _x == 0 ? true : false,
                "isEnd": _x == g_W - 1 ? true : false
            };
        };
        get getRGBA() {
            return this._RGBA;
        };
        get _getFlag() {
            return this._flag;
        };
        setFlag() {
            this._flag = true;
        };
        judge(_RGBA) {
            if (this._getFlag) return false;
            if (this.getRGBA != _RGBA) return false;
            return true;
        };
        static make(_ImageData) {
            const H = _ImageData.height;
            const W = _ImageData.width;
            const array = [];
            for (let y = 0; y < H; y++) {
                array.push([]);
                for (let x = 0; x < W; x++) {
                    const index = (x + y * W) * 4;
                    const R = _ImageData.data[index];
                    const G = _ImageData.data[index + 1];
                    const B = _ImageData.data[index + 2];
                    const A = _ImageData.data[index + 3];
                    array[y].push(new pixelClass(x, y, [R, G, B, A]));
                };
            };
            return array;
        };
        static search_false(_pixelObjArray) {
            if (paintLast_flag.find("input[type='checkbox']").prop("checked")) {
                for (let y = g_H - 1; y >= 0; y--) {
                    for (let x = g_W - 1; x >= 0; x--) {
                        if (_pixelObjArray[y][x]._getFlag == false) return [x, y];
                    };
                };
            } else {
                for (let y = 0; y < g_H; y++) {
                    for (let x = 0; x < g_W; x++) {
                        if (_pixelObjArray[y][x]._getFlag == false) return [x, y];
                    };
                };
            };
            return null;
        };
    };

    function resize(w1, h1) {
        const W_MAX = CV_ELM.width(),
            H_MAX = CV_ELM.height();
        if (w1 <= W_MAX && h1 <= H_MAX) return [w1, h1];
        let w2, h2;
        if (w1 > h1) {
            const ratio = h1 / w1;
            w2 = W_MAX;
            h2 = Math.floor(W_MAX * ratio);
        } else {
            const ratio = w1 / h1;
            h2 = H_MAX;
            w2 = Math.floor(H_MAX * ratio);
        };
        console.log(w2 + " " + h2);
        return [w2, h2];
    };
    const reader = new FileReader();
    reader.addEventListener("load", function() {
        const img = new Image();
        img.src = reader.result;
        img.addEventListener("load", function() {
            Message("読み込み完了");
            const result = resize(img.width, img.height);
            g_W = result[0];
            g_H = result[1];
            const cv = document.createElement("canvas");
            cv.width = g_W;
            cv.height = g_H;
            const ct = cv.getContext("2d");
            ct.drawImage(img, 0, 0, img.width, img.height, 0, 0, g_W, g_H);
            g_pixelClass_Array = pixelClass.make(ct.getImageData(0, 0, cv.width, cv.height));
            alert('画像が読み込み終わりました\n出力する位置をクリックしてください');
            Message('キャンバスのどこかをクリック');
            g_cv_click_flag = true;
        });
    });

    const Auto_Draw = (_Start_x, _Start_y) => {
        const move_log = [];
        move_log.push([_Start_x, _Start_y]);
        const nowRGBA = g_pixelClass_Array[_Start_y][_Start_x].getRGBA;
        setRGBA(nowRGBA.split("_"));
        mouse_event("mousedown", _Start_x + g_ADD_X, _Start_y + g_ADD_Y, CV_ELM);
        let n_x = _Start_x,
            n_y = _Start_y,
            way_log = null,
            break_counter = 0;
        while (1) {
            break_counter++;
            if (loop_max.val() < break_counter) {
                mouse_event("mouseup", 0, 0, CV_ELM); // 中断
                break;
            };
            if (g_stop_flag) return mouse_event("mouseup", 0, 0, CV_ELM);
            const now = g_pixelClass_Array[n_y][n_x];
            now.setFlag();
            const ar = [];
            if (!now.m_y.isFirst) ar.push("up", n_x, n_y - 1);
            if (!now.m_x.isFirst) ar.push("left", n_x - 1, n_y);
            if (!now.m_y.isFirst && !now.m_x.isFirst) ar.push("up_left", n_x - 1, n_y - 1);
            if (!now.m_y.isEnd) ar.push("under", n_x, n_y + 1);
            if (!now.m_x.isEnd) ar.push("right", n_x + 1, n_y);
            if (!now.m_y.isEnd && !now.m_x.isEnd) ar.push("under_right", n_x + 1, n_y + 1);
            if (!now.m_y.isFirst && !now.m_x.isEnd) ar.push("up_right", n_x + 1, n_y - 1);
            if (!now.m_y.isEnd && !now.m_x.isFirst) ar.push("under_left", n_x - 1, n_y + 1);
            const ar2 = [];
            for (let i = 0; i < (ar.length) / 3; i++) {
                if (g_pixelClass_Array[ar[3 * i + 2]][ar[3 * i + 1]].judge(nowRGBA)) ar2.push(ar[3 * i], ar[3 * i + 1], ar[3 * i + 2]);
            }
            if (!ar2.length) {
                if (!move_log.length) {
                    mouse_event("mouseup", 0, 0, CV_ELM); // 探索終了
                    break;
                }
                const move_log_end = move_log.pop();
                n_x = move_log_end[0];
                n_y = move_log_end[1];
                way_log = null;
                mouse_event("mousemove", n_x + g_ADD_X, n_y + g_ADD_Y, CV_ELM);
                continue;
            }
            let next = ar2.indexOf(way_log);
            if (next === -1) next = 0;
            if (1 < ar2.length) move_log.push([n_x, n_y]);
            way_log = ar2[next];
            n_x = ar2[next + 1];
            n_y = ar2[next + 2];
            mouse_event("mousemove", n_x + g_ADD_X, n_y + g_ADD_Y, CV_ELM);
        };
        const result = pixelClass.search_false(g_pixelClass_Array);
        if (result) {
            n_x = result[0];
            n_y = result[1];
            const SIZE = g_W * g_H;
            let now_position = g_W * n_y + n_x;
            if (paintLast_flag.find("input[type='checkbox']").prop("checked")) now_position = SIZE - now_position;
            Message("自動描画中…(" + Math.floor(((now_position / SIZE) * 100) * 1000) / 1000 + "%)");
            setTimeout(function() {
                Auto_Draw(n_x, n_y);
            }, 1000 * input_time.val());
        } else {
            Message("★描画完了(" + new Date().toString().match(/[0-9]{2}:[0-9]{2}:[0-9]{2}/)[0] + ")");
            reset();
            return;
        };
    };

    function reset_input_file() {
        input_file = $("<input>", {
            type: "file"
        }).change(function(e) {
            if (!e.target.files[0]) return;
            Message("画像を読み込み中");
            reader.readAsDataURL(e.target.files[0]);
        });
    };

    function add_stop_button() {
        $(amx.addButton(g_stop_btn_holder[0], {
            title: "緊急停止",
            click: function() {
                g_stop_flag = true;
                Message("緊急停止しました");
                reset();
            }
        })).css("color", "red");
    };
    CV_ELM[0].addEventListener("click", function(e) {
        if (g_cv_click_flag === false) return;
        g_ADD_X = e.clientX;
        g_ADD_Y = e.clientY;
        alert("自動描画を始めます");
        run_flag = true;
        add_stop_button();
        g_stop_flag = false;
        if (paintLast_flag.find("input[type='checkbox']").prop("checked")) Auto_Draw(g_W - 1, g_H - 1);
        else Auto_Draw(0, 0);
        g_cv_click_flag = false;
    }, false);
})(this.unsafeWindow || window);