Home Reference Source

cables_dev/cables_ui/src/ui/dialogs/gradienteditor.js

import { ModalBackground, Logger } from "cables-shared-client";
import { CONSTANTS } from "../../../../cables/src/core/constants.js";
import { getHandleBarHtml } from "../utils/handlebars.js";

/**
 * gradient editor dialog
 *
 * @export
 * @class GradientEditor
 */
export default class GradientEditor
{
    constructor(opid, portname, options)
    {
        this._log = new Logger("gradienteditor");
        this._opId = opid;
        this._portName = portname;

        this._keyWidth =
        this._keyHeight = 7;
        this._keyStrokeWidth = 2;
        // this._keyStrokeWidth = 3;
        this._keyOpacity = 1;
        this._dragDownDeleteThreshold = 120;
        this._width = 512;
        this._height = 100;

        this._oldKeys = [];
        this._keys = [];
        this._paper = null;

        this._movingkey = false;
        this._callback = null;
        this._ctx = null;

        this._currentKey = null;
        this._oldCurrentKey = null;

        this._op = gui.corePatch().getOpById(this._opId);
        this._port = this._op.getPort(this._portName);
        this.type = this._port.uiAttribs.gradientType || "gradient";

        this._anim = new CABLES.Anim();

        this._anim.defaultEasing = CONSTANTS.ANIM.EASING_SMOOTHSTEP;

        this._elContainer = null;
        this._bg = new ModalBackground();
        this._bg.on("click", () =>
        {
            this.close();
        });

        this._options = {};
        this._options.smoothStep = this._port.uiAttribs.gradEditSmoothstep;
        this._options.step = this._port.uiAttribs.gradEditStep;
        this._options.oklab = this._port.uiAttribs.gradOklab;


        this._previousContent = "";
        this._openerEle = (options || {}).openerEle;
    }

    close()
    {
        this._bg.hide();
        this._elContainer.remove();
    }

    selectKey(i)
    {
        this.setCurrentKey(this._keys[i]);
    }

    updateCanvas()
    {
        if (!this._ctx)
        {
            const canvas = ele.byId("gradientEditorCanvas");
            const canvasCurve = ele.byId("gradientEditorCanvasCurve");
            if (!canvas)
            {
                this._log.error("[gradienteditor] no canvas found");
                return;
            }
            this._ctx = canvas.getContext("2d");
            this._ctxCurve = canvasCurve.getContext("2d");
            this._imageData = this._ctx.createImageData(this._width, 1);
        }

        let keys = [];
        if (this._keys.length == 0) keys.push({ "posy": 0.5, "pos": 0, "r": 0, "g": 0, "b": 0 });
        else keys = [{ "posy": this._keys[0].posy, "pos": 0, "r": this._keys[0].r, "g": this._keys[0].g, "b": this._keys[0].b }].concat(this._keys);

        const last = keys[keys.length - 1];
        keys.push({ "posy": last.posy, "pos": 1, "r": last.r, "g": last.g, "b": last.b });

        if (this.type == "curve")
        {
            this._ctxCurve.fillStyle = "#444";
            this._ctxCurve.fillRect(0, 0, this._width, this._height);


            // --------- 0.5 line...

            this._ctxCurve.strokeStyle = "#333";
            this._ctxCurve.beginPath();
            this._ctxCurve.moveTo(0, this._height / 2);
            this._ctxCurve.lineTo(this._width, this._height / 2);
            this._ctxCurve.stroke();

            // --------- linear

            this._ctxCurve.strokeStyle = "#777";
            this._ctxCurve.lineWidth = 1;
            this._ctxCurve.beginPath();
            this._ctxCurve.moveTo(keys[0].pos * this._width, keys[0].posy * this._height - this._keyWidth / 2);

            for (let i = 0; i < keys.length - 1; i++)
                this._ctxCurve.lineTo(
                    Math.floor(keys[i].pos * this._width - this._keyWidth / 2),
                    Math.floor(keys[i].posy * this._height - this._keyWidth / 2 + 1)
                );

            this._ctxCurve.lineTo(keys[keys.length - 1].pos * this._width, keys[keys.length - 1].posy * this._height - this._keyWidth / 2);
            this._ctxCurve.stroke();

            // smoothed...
            // this._ctxCurve.strokeStyle = "#aaa";
            // this._ctxCurve.beginPath();
            // this._ctxCurve.lineWidth = 2;
            // let numSteps = 250;
            // for (let i = 0; i < numSteps + 2; i++)
            // {
            //     let x = Math.floor(i / numSteps * this._width);
            //     let y = Math.floor(this._anim.getValue(i / numSteps) * this._height) - 1;
            //     if (i == 0) this._ctxCurve.moveTo(x, y);
            //     else this._ctxCurve.lineTo(x, y);
            // }
            // this._ctxCurve.stroke();
        }
        else
        {
            for (let i = 0; i < keys.length - 1; i++)
            {
                this._setKeyStyle(keys[i]);
                const keyA = keys[i];
                const keyB = keys[i + 1];

                for (let x = keyA.pos * this._width; x < keyB.pos * this._width; x++)
                {
                    x = Math.round(x);
                    let p = CABLES.map(x, keyA.pos * this._width, keyB.pos * this._width, 0, 1);

                    if (this._options.smoothStep)p = CABLES.smoothStep(p);
                    if (this._options.step)p = Math.round(p);

                    if (this._options.oklab)
                    {
                        const klabA = this.rgbToOklab(keyA.r, keyA.g, keyA.b);
                        const labA_r = klabA[0];
                        const labA_g = klabA[1];
                        const labA_b = klabA[2];

                        const klabB = this.rgbToOklab(keyB.r, keyB.g, keyB.b);
                        const labB_r = klabB[0];
                        const labB_g = klabB[1];
                        const labB_b = klabB[2];

                        const l = ((p * labB_r + (1.0 - p) * labA_r));
                        const a = ((p * labB_g + (1.0 - p) * labA_g));
                        const b = ((p * labB_b + (1.0 - p) * labA_b));

                        const pixCol = this.oklabToRGB(l, a, b);
                        this._imageData.data[x * 4 + 0] = Math.round(pixCol[0] * 255);
                        this._imageData.data[x * 4 + 1] = Math.round(pixCol[1] * 255);
                        this._imageData.data[x * 4 + 2] = Math.round(pixCol[2] * 255);
                        this._imageData.data[x * 4 + 3] = 255;
                    }
                    else
                    {
                        this._imageData.data[x * 4 + 0] = ((p * keyB.r) + (1.0 - p) * (keyA.r)) * 255;
                        this._imageData.data[x * 4 + 1] = ((p * keyB.g) + (1.0 - p) * (keyA.g)) * 255;
                        this._imageData.data[x * 4 + 2] = ((p * keyB.b) + (1.0 - p) * (keyA.b)) * 255;
                        this._imageData.data[x * 4 + 3] = 255;
                    }
                }
            }

            this._ctx.putImageData(this._imageData, 0, 0);
        }

        if (this._opId && this._portName)
        {
            const keyData = [];
            for (let i = 0; i < keys.length; i++)
            {
                keyData[i] =
                {
                    "pos": keys[i].pos,
                    "posy": keys[i].posy,
                    "r": keys[i].r,
                    "g": keys[i].g,
                    "b": keys[i].b
                };
            }

            this._port.set(JSON.stringify({ "keys": keyData }));
        }
    }

    _setKeyStyle(key)
    {
        const attr = {};

        if (key.rect)
        {
            if (this.type == "curve")
            {
                attr.fill = "#888";
                attr.stroke = "#fff";
            }
            else
            {
                attr.fill = "rgba(" + Math.round(key.r * 255) + "," + Math.round(key.g * 255) + "," + Math.round(key.b * 255) + "," + this._keyOpacity + ")";
                attr.stroke = this.getInvStrokeColor(key.r, key.g, key.b);
            }

            key.rect.attr(attr);
        }
    }

    onChange()
    {
        function compare(a, b) { return a.pos - b.pos; }

        this._keys.sort(compare);


        this._anim.clear();
        let html = "";
        for (let i = 0; i < this._keys.length; i++)
        {
            this._keys[i].pos = Math.min(1.0, Math.max(this._keys[i].pos, 0));
            this._keys[i].posy = Math.min(1.0, Math.max(this._keys[i].posy, 0));

            html += "<a data-index=\"" + i + "\" onclick=\"CABLES.GradientEditor.editor.selectKey(" + i + ")\" class=\"keyindex button-small\">" + i + "</a> ";

            this._anim.setValue(this._keys[i].pos, this._keys[i].posy);
        }

        ele.byId("gradienteditorKeys").innerHTML = html;

        this._timeout = setTimeout(
            () =>
            {
                if (CABLES.GradientEditor.editor)CABLES.GradientEditor.editor.updateCanvas();
            }, 3);


        if (this._callback) this._callback();
    }

    deleteKey(k)
    {
        this._keys.splice(this._keys.indexOf(k), 1);
        this.onChange();
    }

    setCurrentKey(key)
    {
        if (this._currentKey) this._currentKey.rect.attr({ "stroke-width": this._keyStrokeWidth });
        this._currentKey = key;

        if (key == this._currentKey) this._currentKey.rect.attr({ "stroke-width": this._keyStrokeWidth * 2 });


        ele.byId("gradientColorInput").style.backgroundColor = "rgb(" + Math.round(key.r * 255) + "," + Math.round(key.g * 255) + "," + Math.round(key.b * 255) + ")";
    }

    getInvStrokeColor(r, g, b)
    {
        if (this.type == "curve") return "rgba(255,255,255,1)";
        let invCol = (r + g + b) / 3;

        if (invCol < 0.5)invCol = 1.0;
        else invCol = 0.0;

        const s = "rgba(" + invCol * 255 + "," + invCol * 255 + "," + invCol * 255 + ",1.0)";
        return s;
    }

    addKey(pos, posy, r, g, b)
    {
        if (r == undefined)
        {
            r = Math.random();
            g = Math.random();
            b = Math.random();
        }

        const rect = this._paper.ellipse(
            pos * this._width - this._keyWidth / 2,
            posy * this._height - this._keyWidth / 2,
            this._keyWidth,
            this._keyHeight).attr(
            {
                "fill": "transparent",
                "stroke": this.getInvStrokeColor(r, g, b),
                "stroke-width": this._keyStrokeWidth
            });

        const key = { "posy": posy, "pos": pos, "rect": rect, "r": r, "g": g, "b": b };

        this._setKeyStyle(key);

        this._keys.push(key);
        let shouldDelete = false;
        this.setCurrentKey(key);

        const move = (dx, dy, x, y, e) =>
        {
            this.setCurrentKey(key);
            this._movingkey = true;
            const attribs = {};

            attribs.stroke = this.getInvStrokeColor(key.r, key.g, key.b);

            // e.target == key.rect.node || //|| e.target.tagName == "circle"
            if (e.target.tagName == "svg" || e.target.tagName == "circle" || e.target.tagName == "ellipse")
            {
                let eX = e.offsetX - (this._keyWidth / 2);
                let eY = e.offsetY - (this._keyWidth / 2);

                eX = Math.max(eX, 0);
                eY = Math.max(eY, 0);
                eX = Math.min(eX, this._width);
                eY = Math.min(eY, this._height);

                attribs.cx = eX;
                attribs.cy = eY;

                key.pos = (eX + (this._keyWidth / 2)) / this._width;
                key.posy = (eY + (this._keyWidth / 2)) / this._height;
            }

            rect.attr(attribs);
            this.onChange();
        };

        const down = (x, y, e) =>
        {
            try { e.target.setPointerCapture(e.pointerId); }
            catch (_e) {}

            if (e.buttons == 2) shouldDelete = true;

            this._startMouseY = y;
            this._movingkey = true;
            this.setCurrentKey(key);
        };

        const up = (e) =>
        {
            try { e.target.releasePointerCapture(e.pointerId); }
            catch (_e) {}

            setTimeout(() =>
            {
                this._movingkey = false;
            }, 100);

            if (shouldDelete && key.rect)
            {
                key.rect.remove();
                this.deleteKey(key);
            }
        };

        if (rect)rect.drag(move, down, up);
    }

    show(cb)
    {
        this._callback = cb;

        if (window.gui && gui.currentModal) gui.currentModal.close();

        const html = getHandleBarHtml("GradientEditor", { "name": this._portName });

        this._bg.show(true);

        this._elContainer = document.createElement("div");
        this._elContainer.classList.add("gradientEditorContainer");
        this._elContainer.classList.add("cablesCssUi");

        document.body.appendChild(this._elContainer);
        this._elContainer.innerHTML = html;


        if (this._openerEle)
        {
            const r = this._openerEle.getBoundingClientRect();
            const rge = this._elContainer.getBoundingClientRect();

            this._elContainer.style.left = r.x - rge.width - 20 + "px";
            this._elContainer.style.top = r.y + "px";
        }
        else
        {
            this._elContainer.style.left = 100 + "px";
            this._elContainer.style.top = 100 + "px";
        }

        this._paper = Raphael("gradienteditorbar", 0, 0);

        document.querySelector("#gradienteditorbar svg").addEventListener("pointerdown", (e) =>
        {
            try { e.target.setPointerCapture(e.pointerId); }
            catch (_e) {}
        });


        document.querySelector("#gradienteditorbar svg").addEventListener("pointerup", (e) =>
        {
            try { e.target.releasePointerCapture(e.pointerId); }
            catch (_e) {}
        });


        document.querySelector("#gradienteditorbar svg").addEventListener("click", (e) =>
        {
            if (this._movingkey) return;
            this.addKey(e.offsetX / this._width, e.offsetY / this._height);
            this.onChange();
        });

        if (this._opId && this._portName)
        {
            const op = gui.corePatch().getOpById(this._opId);
            const data = op.getPort(this._portName).get();
            try
            {
                this._previousContent = data;
                const keys = JSON.parse(data).keys || [];
                for (let i = 1; i < keys.length - 1; i++)
                    this.addKey(keys[i].pos, keys[i].posy, keys[i].r, keys[i].g, keys[i].b);
            }
            catch (e)
            {
                this._log.error(e);
            }
        }

        if (this._keys.length == 0)
        {
            this.addKey(0, 0.5, 0, 0, 0);
            this.addKey(1, 0.5, 1, 1, 1);
        }

        this.onChange();
        CABLES.GradientEditor.editor = this;

        ele.byId("gradientSaveButton").addEventListener("click", () =>
        {
            this.close();
        });

        ele.byId("gradientCancelButton").addEventListener("click", () =>
        {
            const op = gui.corePatch().getOpById(this._opId);
            op.getPort(this._portName).set(this._previousContent);
            this.close();
        });

        const colEleDel = ele.byId("gradientColorDelete");
        colEleDel.addEventListener("click", (e) =>
        {
            if (this._currentKey)
            {
                this._currentKey.rect.remove();
                this.deleteKey(this._currentKey);
                this._currentKey = this._keys[0];
            }
        });

        if (this.type == "curve")ele.byId("gradientColorInput").classList.add("hidden");
        else ele.byId("gradientColorInput").classList.remove("hidden");

        const colEle = ele.byId("gradientColorInput");

        colEle.addEventListener("click", (e) =>
        {
            if (!this._currentKey) return;
            const cr = new ColorRick({
                "ele": colEle,
                "color": [parseInt(this._currentKey.r * 255), parseInt(this._currentKey.g * 255), parseInt(this._currentKey.b * 255)], // "#ffffff",
                "onChange": (col) =>
                {
                    if (this._currentKey)
                    {
                        this._currentKey.r = col.gl()[0];
                        this._currentKey.g = col.gl()[1];
                        this._currentKey.b = col.gl()[2];

                        CABLES.GradientEditor.editor._ctx = null;
                        CABLES.GradientEditor.editor.onChange();

                        colEle.style.backgroundColor = col.hex();
                    }
                }
            });
        });
    }


    rgbToOklab(r, g, b)
    {
        let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
        let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
        let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
        l = Math.cbrt(l); m = Math.cbrt(m); s = Math.cbrt(s);
        return [
            l * +0.2104542553 + m * +0.7936177850 + s * -0.0040720468,
            l * +1.9779984951 + m * -2.4285922050 + s * +0.4505937099,
            l * +0.0259040371 + m * +0.7827717662 + s * -0.8086757660
        ];
    }

    clamp(value, min, max)
    {
        return Math.max(Math.min(value, max), min);
    }

    oklabToRGB(L, a, b)
    {
        let l = L + a * +0.3963377774 + b * +0.2158037573;
        let m = L + a * -0.1055613458 + b * -0.0638541728;
        let s = L + a * -0.0894841775 + b * -1.2914855480;
        l **= 3; m **= 3; s **= 3;
        let rgb_r = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292;
        let rgb_g = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965;
        let rgb_b = l * -0.0041960863 + m * -0.7034186147 + s * +1.7076147010;
        rgb_r = CABLES.clamp(rgb_r, 0, 1); rgb_g = CABLES.clamp(rgb_g, 0, 1); rgb_b = CABLES.clamp(rgb_b, 0, 1);
        return [rgb_r, rgb_g, rgb_b];
    }
}