Home Reference Source

scripts/widgets/ui_colorpicker2.js

"use strict";

import * as util from '../path-controller/util/util.js';
import * as vectormath from '../path-controller/util/vectormath.js';
import * as ui_base from '../core/ui_base.js';
import * as events from '../path-controller/util/events.js';
import * as ui from '../core/ui.js';
import {PropTypes} from '../path-controller/toolsys/toolprop.js';
import {keymap} from '../path-controller/util/simple_events.js';
import cconst from '../config/const.js';
import {color2web, web2color, validateWebColor} from "../core/ui_base.js";

let Vector2 = vectormath.Vector2,
    Vector3 = vectormath.Vector3,
    Vector4 = vectormath.Vector4,
    Matrix4 = vectormath.Matrix4;

export {rgb_to_hsv, hsv_to_rgb} from "../path-controller/util/colorutils.js";
import {rgb_to_hsv, hsv_to_rgb, cmyk_to_rgb, rgb_to_cmyk} from "../path-controller/util/colorutils.js";
import {contextWrangler} from '../screen/area_wrangler.js';

let UIBase     = ui_base.UIBase,
    PackFlags  = ui_base.PackFlags,
    IconSheets = ui_base.IconSheets;

let UPW = 1.25, VPW = 0.75;

//*
let sample_rets = new util.cachering(() => [0, 0], 64);

export function inv_sample(u, v) {
  let ret = sample_rets.next();

  ret[0] = Math.pow(u, UPW);
  ret[1] = Math.pow(v, VPW);

  return ret;
}

export function sample(u, v) {
  let ret = sample_rets.next();

  ret[0] = Math.pow(u, 1.0/UPW);
  ret[1] = Math.pow(v, 1.0/VPW);

  return ret;
}

//*/

/* shared methods for clipboard handling */
function colorClipboardCopy() {
  let rgba = this.getRGBA();

  let r = rgba[0]*255;
  let g = rgba[1]*255;
  let b = rgba[2]*255;
  let a = rgba[3];

  let data = `rgba(${r.toFixed(4)}, ${g.toFixed(4)}, ${b.toFixed(4)}, ${a.toFixed(4)})`;
  //cconst.setClipboardData("color", "text/css", data);

  cconst.setClipboardData("color", "text/plain", data);
}

function colorClipboardPaste() {
  let data = cconst.getClipboardData("text/plain");

  if (!data || !validateCSSColor("" + data.data)) {// || data.mime !== "text/css") {
    return;
  }

  let color;

  try {
    color = css2color(data.data);
  } catch (error) {
    //not a color
    console.log(error.stack);
    console.log(error.message);
  }

  if (color) {
    if (color.length < 4) {
      color.push(1.0);
    }

    this.setRGBA(color);
  }
}

let fieldrand = new util.MersenneRandom(0);

let huefields = {};

export function getHueField(width, height, dpi) {
  let key = width + ":" + height + ":" + dpi.toFixed(4);

  if (key in huefields) {
    return huefields[key];
  }

  let field = new ImageData(width, height);
  let idata = field.data;

  for (let i = 0; i < width*height; i++) {
    let ix = i%width, iy = ~~(i/width);
    let idx = i*4;

    let rgb = hsv_to_rgb(ix/width, 1, 1);

    idata[idx] = rgb[0]*255;
    idata[idx + 1] = rgb[1]*255;
    idata[idx + 2] = rgb[2]*255;
    idata[idx + 3] = 255;
  }

  //*
  let canvas = document.createElement("canvas");
  canvas.width = field.width;
  canvas.height = field.height;
  let g = canvas.getContext("2d");
  g.putImageData(field, 0, 0);
  field = canvas;
  //*/

  huefields[key] = field;
  return field;
}

let fields = {};

export function getFieldImage(fieldsize, width, height, hsva) {
  fieldrand.seed(0);

  /* render field at half res and upscale via canvas2d */
  let width2 = width>>1;
  let height2 = height>>1;
  let fieldsize2 = fieldsize>>1;

  let hue = hsva[0];
  let hue_rgb = hsv_to_rgb(hue, 1.0, 1.0);
  let key = fieldsize + ":" + width2 + ":" + height2 + ":" + hue.toFixed(5);

  if (key in fields)
    return fields[key];

  let size2 = fieldsize2;
  let valpow = 0.75;

  let image = {
    width : width,
    height: height,
    image : new ImageData(fieldsize2, fieldsize2),

    x2sat: (x) => {
      return Math.min(Math.max(x/width, 0), 1);
    },
    y2val: (y) => {
      y = 1.0 - Math.min(Math.max(y/height, 0), 1);

      return y === 0.0 ? 0.0 : y**valpow;
    },
    sat2x: (s) => {
      return s*width;
    },
    val2y: (v) => {
      if (v === 0)
        return height;

      v = v**(1.0/valpow);
      return (1.0 - v)*height;
    }
  };

  image.params = {
    box: {
      x     : 0,
      y     : 0,
      width : width,
      height: height
    }
  };

  let idata = image.image.data;
  for (let i = 0; i < idata.length; i += 4) {
    let i2 = i/4;
    let x = i2%size2, y = ~~(i2/size2);

    let v = 1.0 - (y/size2);
    let s = (x/size2);

    let rgb = hsv_to_rgb(hsva[0], s, v**valpow);

    idata[i] = rgb[0]*255;
    idata[i + 1] = rgb[1]*255;
    idata[i + 2] = rgb[2]*255;
    idata[i + 3] = 255;
  }

  //*
  let image2 = document.createElement("canvas");
  image2.width = size2;
  image2.height = size2;
  let g = image2.getContext("2d");
  g.putImageData(image.image, 0, 0);
  //*/
  image.canvas = image2;
  image.scale = width/size2;

  fields[key] = image;
  return image;
}


let _update_temp = new Vector4();

export class SimpleBox {
  constructor(pos = [0, 0], size = [1, 1]) {
    this.pos = new Vector2(pos);
    this.size = new Vector2(size);
    this.r = 0;
  }
}

export class HueField extends UIBase {
  constructor() {
    super();

    this.canvas = document.createElement("canvas");
    this.g = this.canvas.getContext("2d");
    this.shadow.appendChild(this.canvas);

    let setFromXY = (x, y) => {
      let dpi = this.getDPI();
      let pad = this._getPad();

      let w = this.canvas.width/dpi - pad*2.0;
      x -= pad;

      let h = x/w;
      h = Math.min(Math.max(h, 0.0), 1.0);

      this.hsva[0] = h;

      if (this.onchange) {
        this.onchange(this.hsva);
      }

      this._redraw();
    };

    this.addEventListener("keydown", (e) => {
      switch (e.keyCode) {
        case keymap["Left"]:
        case keymap["Right"]:
          let sign = e.keyCode === keymap["Left"] ? -1 : 1;

          this.hsva[0] = Math.min(Math.max(this.hsva[0] + 0.05*sign, 0.0), 1.0);
          this._redraw();

          if (this.onchange) {
            this.onchange();
          }
          break;
      }
    });

    this.addEventListener("mousedown", (e) => {
      if (this.modalRunning) {
        return;
      }

      /* ensure browser doesn't spawn its own (incompatible)
         touch->mouse emulation events}; */
      e.preventDefault();

      let rect = this.canvas.getClientRects()[0];
      let x = e.clientX - rect.x, y = e.clientY - rect.y;

      setFromXY(x, y);

      this.pushModal({
        on_mousemove: (e) => {
          let rect = this.canvas.getClientRects()[0];
          let x = e.clientX - rect.x, y = e.clientY - rect.y;

          setFromXY(x, y);
        },
        on_mousedown: (e) => {
          this.popModal();
        },
        on_mouseup  : (e) => {
          this.popModal();
        },
        on_keydown  : (e) => {
          if (e.keyCode === keymap["Enter"] || e.keyCode === keymap["Escape"] || e.keyCode === keymap["Space"]) {
            this.popModal();
          }
        }
      });
    });
  }


  static define() {
    return {
      tagname          : "huefield-x",
      style            : "colorfield",
      havePickClipboard: true
    };
  }

  getRGBA() {
    let rgb = hsv_to_rgb(this.hsva[0], this.hsva[1], this.hsva[2]);
    return new Vector4().loadXYZW(rgb[0], rgb[1], rgb[2], this.hsva[3]);
  }

  setRGBA(rgba) {
    let hsv = rgb_to_hsv(rgba[0], rgba[1], rgba[2]);
    this.hsva.loadXYZW(hsv[0], hsv[1], hsv[2], rgba[3]);

    this._redraw();

    if (this.onchange) {
      this.onchange(this.hsva);
    }
  }

  clipboardCopy() {
    colorClipboardCopy.apply(this, arguments);
  }

  clipboardPaste() {
    colorClipboardPaste.apply(this, arguments);
  }

  _getPad() {
    return Math.max(this.getDefault("circleSize"), 15);
  }

  _redraw() {
    let g = this.g, canvas = this.canvas;
    let dpi = this.getDPI();

    let w = this.getDefault("width");
    let h = this.getDefault("hueHeight");

    canvas.width = ~~(w*dpi);
    canvas.height = ~~(h*dpi);

    canvas.style["width"] = w + "px";
    canvas.style["height"] = h + "px";

    /* create horizontal padding to make selection of
     *  endpoint hue easier */

    let rselector = ~~(this._getPad()*dpi); //~~(this.getDefault("circleSize")*dpi);
    let r_circle = this.getDefault("circleSize")*dpi;

    let w2 = canvas.width, h2 = canvas.height;

    w2 -= rselector*2.0;

    //g.drawImage(getHueField(w2, h2, dpi), 0, 0, w2, h2, rselector*2, 0, w2, h2);
    g.drawImage(getHueField(w2, h2, dpi), 0, 0, w2, h2, rselector, 0, w2, h2);

    let x = this.hsva[0]*w2 + rselector;
    let y = canvas.height*0.5;

    g.beginPath();
    g.arc(x, y, r_circle, -Math.PI, Math.PI);
    g.closePath();

    g.strokeStyle = "white";
    g.lineWidth = 3*dpi;
    g.stroke();

    g.strokeStyle = "grey";
    g.lineWidth = 1*dpi;
    g.stroke();

    if (this.disabled) {
      g.beginPath();
      g.fillStyle = "rgba(25,25,25,0.75)"
      g.rect(0, 0, this.canvas.width, this.canvas.height);
      g.fill();
    }
  }

  on_disabled() {
    this._redraw();
  }

  on_enabled() {
    this._redraw();
  }
}

UIBase.internalRegister(HueField);

export class SatValField extends UIBase {
  constructor() {
    super();

    this.hsva = [0, 0, 0, 1];

    this.canvas = document.createElement("canvas");
    this.g = this.canvas.getContext("2d");
    this.shadow.appendChild(this.canvas);

    this.onchange = undefined;

    let setFromXY = (x, y) => {
      let field = this._getField();
      let r = ~~(this.getDefault("circleSize")*this.getDPI());

      let sat = field.x2sat(x - r);
      let val = field.y2val(y - r);

      this.hsva[1] = sat;
      this.hsva[2] = val;

      if (this.onchange) {
        this.onchange(this.hsva);
      }

      this._redraw();
    };

    this.addEventListener("keydown", (e) => {
      switch (e.keyCode) {
        case keymap["Left"]:
        case keymap["Right"]: {
          let sign = e.keyCode === keymap["Left"] ? -1 : 1;

          this.hsva[1] = Math.min(Math.max(this.hsva[1] + 0.05*sign, 0.0), 1.0);
          this._redraw();

          if (this.onchange) {
            this.onchange(this.hsva);
          }
          break;
        }

        case keymap["Up"]:
        case keymap["Down"]: {
          let sign = e.keyCode === keymap["Down"] ? -1 : 1;

          this.hsva[2] = Math.min(Math.max(this.hsva[2] + 0.05*sign, 0.0), 1.0);
          this._redraw();

          if (this.onchange) {
            this.onchange(this.hsva);
          }
          break;
        }
      }
    });

    this.canvas.addEventListener("mousedown", (e) => {
      if (this.modalRunning) {
        return;
      }

      /* ensure browser doesn't spawn its own (incompatible)
         touch->mouse emulation events}; */
      e.preventDefault();

      let rect = this.canvas.getClientRects()[0];
      let x = e.clientX - rect.x, y = e.clientY - rect.y;

      setFromXY(x, y);

      this.pushModal({
        on_pointermove(e) {
          this.on_mousemove(e);
        },
        on_mousemove: (e) => {
          let rect = this.canvas.getClientRects()[0];
          if (rect === undefined) {
            return;
          }

          let x = e.clientX - rect.x, y = e.clientY - rect.y;

          setFromXY(x, y);
        },
        on_mousedown: (e) => {
          this.popModal();
        },
        on_mouseup  : (e) => {
          this.popModal();
        },
        on_keydown  : (e) => {
          if (e.keyCode === keymap["Enter"] || e.keyCode === keymap["Escape"] || e.keyCode === keymap["Space"]) {
            this.popModal();
          }
        }
      });
    });

    this.canvas.addEventListener("touchstart", (e) => {
      if (this.modalRunning) {
        return;
      }

      /* ensure browser doesn't spawn its own (incompatible)
         touch->mouse emulation events}; */
      e.preventDefault();

      let rect = this.canvas.getClientRects()[0];
      let x = e.touches[0].clientX - rect.x, y = e.touches[0].clientY - rect.y;

      setFromXY(x, y);

      this.pushModal({
        on_mousemove  : (e) => {
          let rect = this.canvas.getClientRects()[0];
          let x, y;

          if (e.touches && e.touches.length) {
            x = e.touches[0].clientX - rect.x;
            y = e.touches[0].clientY - rect.y;
          } else {
            x = e.x;
            y = e.y;
          }

          setFromXY(x, y);
        },
        on_touchmove  : (e) => {
          let rect = this.canvas.getClientRects()[0];
          let x = e.touches[0].clientX - rect.x, y = e.touches[0].clientY - rect.y;

          setFromXY(x, y);
        },
        on_mousedown  : (e) => {
          this.popModal();
        },
        on_touchcancel: (e) => {
          this.popModal();
        },
        on_touchend   : (e) => {
          this.popModal();
        },
        on_mouseup    : (e) => {
          this.popModal();
        },
        on_keydown    : (e) => {
          if (e.keyCode === keymap["Enter"] || e.keyCode === keymap["Escape"] || e.keyCode === keymap["Space"]) {
            this.popModal();
          }
        }
      });
    })
  }

  static define() {
    return {
      tagname          : "satvalfield-x",
      style            : "colorfield",
      havePickClipboard: true
    };
  }

  getRGBA() {
    let rgb = hsv_to_rgb(this.hsva[0], this.hsva[1], this.hsva[2]);
    return new Vector4().loadXYZW(rgb[0], rgb[1], rgb[2], this.hsva[3]);
  }

  setRGBA(rgba) {
    let hsv = rgb_to_hsv(rgba[0], rgba[1], rgba[2]);
    this.hsva.loadXYZW(hsv[0], hsv[1], hsv[2], rgba[3]);

    this.update(true);
    this._redraw();

    if (this.onchange) {
      this.onchange(this.hsva);
    }
  }

  clipboardCopy() {
    colorClipboardCopy.apply(this, arguments);
  }

  clipboardPaste() {
    colorClipboardPaste.apply(this, arguments);
  }

  _getField() {
    let dpi = this.getDPI();
    let canvas = this.canvas;
    let r = this.getDefault("circleSize");
    let w = this.getDefault("width");
    let h = this.getDefault("height");

    //r = ~~(r*dpi);

    return getFieldImage(this.getDefault("fieldSize"), w - r*2, h - r*2, this.hsva);
  }

  update(force_update = false) {
    super.update();

    if (force_update) {
      this._redraw();
    }
  }

  _redraw() {
    let g = this.g, canvas = this.canvas;
    let dpi = this.getDPI();

    let w = this.getDefault("width");
    let h = this.getDefault("height");

    canvas.width = ~~(w*dpi);
    canvas.height = ~~(h*dpi);

    canvas.style["width"] = w + "px";
    canvas.style["height"] = h + "px";
    //SatValField

    let rselector = ~~(this.getDefault("circleSize")*dpi);

    let field = this._getField()
    let image = field.canvas;

    g.globalAlpha = 1.0;
    g.beginPath();
    g.rect(0, 0, canvas.width, canvas.height);
    g.fillStyle = "rgb(200, 200, 200)";
    g.fill();

    g.beginPath();

    let steps = 17;
    let dx = canvas.width/steps;
    let dy = canvas.height/steps;

    for (let i = 0; i < steps*steps; i++) {
      let x = (i%steps)*dx, y = (~~(i/steps))*dy;

      if (i%2 === 0) {
        continue;
      }
      g.rect(x, y, dx, dy);
    }

    g.fillStyle = "rgb(110, 110, 110)";
    g.fill();

    g.globalAlpha = this.hsva[3];
    g.drawImage(image, 0, 0, image.width, image.height, rselector, rselector, canvas.width - rselector*2, canvas.height - rselector*2);

    let hsva = this.hsva;

    let x = field.sat2x(hsva[1])*dpi + rselector;
    let y = field.val2y(hsva[2])*dpi + rselector;
    let r = rselector;

    g.beginPath();
    g.arc(x, y, r, -Math.PI, Math.PI);
    g.closePath();

    g.strokeStyle = "white";
    g.lineWidth = 3*dpi;
    g.stroke();

    g.strokeStyle = "grey";
    g.lineWidth = 1*dpi;
    g.stroke();

    if (this.disabled) {
      g.beginPath();
      g.fillStyle = "rgba(25,25,25,0.75)";
      g.rect(0, 0, this.canvas.width, this.canvas.height);
      g.fill();
    }
  }

  on_disabled() {
    this._redraw();
  }

  on_enabled() {
    this._redraw();
  }
}

UIBase.internalRegister(SatValField);

export class ColorField extends ui.ColumnFrame {
  constructor() {
    super();

    this.hsva = new Vector4([0.05, 0.6, 0.15, 1.0]);
    this.rgba = new Vector4([0, 0, 0, 0]);

    this._recalcRGBA();

    this._lastThemeStyle = this.constructor.define().style;

    /*
    this.hbox = new SimpleBox();
    this.svbox = new SimpleBox();
    //*/

    this._last_dpi = undefined;

    let satvalfield = this.satvalfield = UIBase.createElement("satvalfield-x");
    satvalfield.hsva = this.hsva;

    let huefield = this.huefield = UIBase.createElement("huefield-x");
    huefield.hsva = this.hsva;

    huefield.onchange = (e) => {
      this.satvalfield._redraw();
      this._recalcRGBA();

      if (this.onchange) {
        this.onchange(this.rgba);
      }
    };

    satvalfield.onchange = (e) => {
      this._recalcRGBA();

      if (this.onchange) {
        this.onchange(this.rgba);
      }
    };

    this._add(satvalfield);
    this._add(huefield);
    //this.shadow.appendChild(canvas);
    //this.shadow.appendChild(huecanvas);
  }

  static define() {
    return {
      tagname: "colorfield-x",
      style  : "colorfield"
    };
  }

  setCMYK(c, m, y, k) {
    let rgb = cmyk_to_rgb(c, m, y, k);
    let hsv = rgb_to_hsv(rgb[0], rgb[1], rgb[2]);

    this.setHSVA(hsv[0], hsv[1], hsv[2], this.hsva[3]);
  }

  getCMYK() {
    let rgb = hsv_to_rgb(this.hsva[0], this.hsva[1], this.hsva[2]);
    return rgb_to_cmyk(rgb[0], rgb[1], rgb[2]);
  }

  setHSVA(h, s, v, a = 1.0, fire_onchange = true) {
    this.hsva[0] = h;
    this.hsva[1] = s;
    this.hsva[2] = v;
    this.hsva[3] = a;

    this._recalcRGBA();
    this.update(true);

    if (this.onchange && fire_onchange) {
      this.onchange(this.hsva, this.rgba);
    }
  }

  _recalcRGBA() {
    let ret = hsv_to_rgb(this.hsva[0], this.hsva[1], this.hsva[2]);

    this.rgba[0] = ret[0];
    this.rgba[1] = ret[1];
    this.rgba[2] = ret[2];
    this.rgba[3] = this.hsva[3];

    return this;
  }

  updateDPI(force_update = false, _in_update = false) {
    let dpi = this.getDPI();

    let update = force_update;
    update = update || dpi != this._last_dpi;

    if (update) {
      this._last_dpi = dpi;

      if (!_in_update)
        this._redraw();

      return true;
    }
  }

  getRGBA() {
    let rgb = hsv_to_rgb(this.hsva[0], this.hsva[1], this.hsva[2]);
    return new Vector4().loadXYZW(rgb[0], rgb[1], rgb[2], this.hsva[3]);
  }

  setRGBA(r, g, b, a = 1.0, fire_onchange = true) {
    if (typeof r === "object") {
      g = r[1];
      b = r[2];
      a = r[3];
      r = r[0];
    }

    let hsv = rgb_to_hsv(r, g, b);

    this.hsva[0] = hsv[0];
    this.hsva[1] = hsv[1];
    this.hsva[2] = hsv[2];
    this.hsva[3] = a;

    this._recalcRGBA();
    this.update(true);

    if (this.onchange && fire_onchange) {
      this.onchange(this.hsva, this.rgba);
    }
  }

  updateThemeOverride() {
    let theme = this.getStyleClass();
    if (theme === this._lastThemeStyle) {
      return false;
    }

    this._lastThemeStyle = theme;
    this.huefield.overrideClass(theme);
    this.satvalfield.overrideClass(theme);

    for (let i=0; i<3; i++) {
      this.flushSetCSS();
      this.flushUpdate();
    }

    return true;
  }

  update(force_update = false) {
    super.update();

    this.updateThemeOverride();

    let redraw = this.updateDPI(force_update, true);
    redraw = redraw || force_update;

    if (redraw) {
      this.satvalfield.update(true);
      this._redraw();
    }
  }

  setCSS() {
    super.setCSS();

    this.style["flex-grow"] = this.getDefault("flex-grow");
  }

  _redraw() {
    this.satvalfield._redraw();
    this.huefield._redraw();
  }
}

UIBase.internalRegister(ColorField);

export class ColorPicker extends ui.ColumnFrame {
  constructor() {
    super();

    this._lastThemeStyle = this.constructor.define().style;
  }

  //*
  get hsva() {
    return this.field.hsva;
  }

  get rgba() {
    return this.field.rgba;
  }//*/

  set description(val) {
    //do not allow setting description of the colorpicker container
  }

  static setDefault(node) {
    let tabs, colorsPanel = node;

    if (node.getClassDefault("usePanels")) {
      let panel = colorsPanel = node.panel("Color");
      tabs = panel.tabs();
      panel.closed = true;

      panel.style["flex-grow"] = "unset";

      /* force compactness */
      panel.titleframe.style["flex-grow"] = "unset";
      //panel.titleframe.style["align-items"] = "unset";
    } else {
      tabs = node.tabs();
    }

    node.cssText = node.textbox();
    node.cssText.onchange = (val) => {
      let ok = validateWebColor(val);
      if (!ok) {
        node.cssText.flash("red");
        return;
      } else {
        node.cssText.flash("green");
      }

      val = val.trim();

      let color = web2color(val);

      node._no_update_textbox = true;
      node.field.setRGBA(color[0], color[1], color[2], color[3]);
      node._setSliders();

      node._no_update_textbox = false;
    };

    //tabs.overrideDefault("background-color", node.getDefault("background-color"));

    let tab = tabs.tab("HSV");

    node.h = tab.slider(undefined, "Hue", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let hsva = node.hsva;
      node.setHSVA(e.value, hsva[1], hsva[2], hsva[3]);
    });

    node.s = tab.slider(undefined, "Saturation", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let hsva = node.hsva;
      node.setHSVA(hsva[0], e.value, hsva[2], hsva[3]);
    });
    node.v = tab.slider(undefined, "Value", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let hsva = node.hsva;
      node.setHSVA(hsva[0], hsva[1], e.value, hsva[3]);
    });
    node.a = tab.slider(undefined, "Alpha", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let hsva = node.hsva;
      node.setHSVA(hsva[0], hsva[1], hsva[2], e.value);
    });

    node.h.baseUnit = node.h.displayUnit = "none";
    node.s.baseUnit = node.s.displayUnit = "none";
    node.v.baseUnit = node.v.displayUnit = "none";
    node.a.baseUnit = node.a.displayUnit = "none";

    tab = tabs.tab("RGB");

    node.r = tab.slider(undefined, "R", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let rgba = node.rgba;
      node.setRGBA(e.value, rgba[1], rgba[2], rgba[3]);
    });
    node.g = tab.slider(undefined, "G", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let rgba = node.rgba;
      node.setRGBA(rgba[0], e.value, rgba[2], rgba[3]);
    });
    node.b = tab.slider(undefined, "B", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let rgba = node.rgba;
      node.setRGBA(rgba[0], rgba[1], e.value, rgba[3]);
    });
    node.a2 = tab.slider(undefined, "Alpha", 0.0, 0.0, 1.0, 0.001, false, true, (e) => {
      let rgba = node.rgba;
      node.setRGBA(rgba[0], rgba[1], rgba[2], e.value);
    });

    node.r.baseUnit = node.r.displayUnit = "none";
    node.g.baseUnit = node.g.displayUnit = "none";
    node.b.baseUnit = node.b.displayUnit = "none";
    node.a2.baseUnit = node.a2.displayUnit = "none";

    if (!node.getDefault("noCMYK")) {
      tab = tabs.tab("CMYK")
      let cmyk = node.getCMYK();

      let makeCMYKSlider = (label, idx) => {
        let slider = tab.slider(undefined, {
          name      : label,
          min       : 0.0,
          max       : 1.0,
          is_int    : false,
          defaultval: cmyk[idx],
          callback  : (e) => {
            let cmyk = node.getCMYK();
            cmyk[idx] = e.value;
            node.setCMYK(cmyk[0], cmyk[1], cmyk[2], cmyk[3]);
          },
          step      : 0.001
        });

        slider.baseUnit = slider.displayUnit = "none";

        return slider;
      }

      node.cmyk = [
        makeCMYKSlider("C", 0),
        makeCMYKSlider("M", 1),
        makeCMYKSlider("Y", 2),
        makeCMYKSlider("K", 3),
      ];
    }

    node._setSliders();
  }

  static define() {
    return {
      tagname            : "colorpicker-x",
      style              : "colorfield",
      havePickClipboard  : true,
      copyForAllChildren : true,
      pasteForAllChildren: true,
    };
  }

  clipboardCopy() {
    colorClipboardCopy.apply(this, arguments);
  }

  clipboardPaste() {
    colorClipboardPaste.apply(this, arguments);
  }

  init() {
    super.init();

    this.field = UIBase.createElement("colorfield-x");
    this.field.setAttribute("class", "colorpicker");

    this.field.packflag |= this.inherit_packflag;
    this.field.packflag |= this.packflag;

    this.field.onchange = () => {
      this._setDataPath();
      this._setSliders();

      if (this.onchange) {
        this.onchange(this.field.rgba);
      }
    };

    let style = document.createElement("style");
    style.textContent = `
      .colorpicker {
        background-color : ${this.getDefault("background-color")};
      }
    `;

    this._style = style;

    let cb = this.colorbox = document.createElement("div");
    cb.style["width"] = "100%";
    cb.style["height"] = this.getDefault("colorBoxHeight") + "px";
    cb.style["background-color"] = "black";

    this.shadow.appendChild(style);
    this.field.ctx = this.ctx;

    this.add(this.colorbox);
    this.add(this.field);

    this.style["width"] = this.getDefault("width") + "px";
  }

  updateColorBox() {
    let r = this.field.rgba[0], g = this.field.rgba[1], b = this.field.rgba[2];
    //let a = this.field.rgba[3];

    r = ~~(r*255);
    g = ~~(g*255);
    b = ~~(b*255);

    let css = `rgb(${r},${g},${b})`;
    this.colorbox.style["background-color"] = css;
  }

  _setSliders() {
    if (this.h === undefined) {
      //setDefault() wasn't called
      console.warn("colorpicker ERROR");
      return;
    }

    let hsva = this.field.hsva;
    this.h.setValue(hsva[0], false);
    this.s.setValue(hsva[1], false);
    this.v.setValue(hsva[2], false);
    this.a.setValue(hsva[3], false);

    let rgba = this.field.rgba;

    this.r.setValue(rgba[0], false);
    this.g.setValue(rgba[1], false);
    this.b.setValue(rgba[2], false);
    this.a2.setValue(rgba[3], false);

    if (this.cmyk) {
      let cmyk = this.field.getCMYK();

      for (let i = 0; i < 4; i++) {
        this.cmyk[i].setValue(cmyk[i], false);
      }
    }

    this.updateColorBox();

    if (!this._no_update_textbox) {
      this.cssText.text = color2web(this.field.rgba);
    }
  }

  updateDataPath() {
    if (!this.hasAttribute("datapath")) {
      return;
    }

    let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));
    let val = this.getPathValue(this.ctx, this.getAttribute("datapath"));

    if (val === undefined) {
      //console.warn("Bad datapath", this.getAttribute("datapath"));
      this.internalDisabled = true;
      return;
    }

    this.internalDisabled = false;

    _update_temp.load(val);

    if (prop.type === PropTypes.VEC3) {
      _update_temp[3] = 1.0;
    }

    if (_update_temp.vectorDistance(this.field.rgba) > 0.01) {
      this.field.setRGBA(_update_temp[0], _update_temp[1], _update_temp[2], _update_temp[3], false);
      this._setSliders();
      this.field.update(true);
    }
  }

  updateThemeOverride() {
    let theme = this.getStyleClass();
    if (theme === this._lastThemeStyle) {
      return false;
    }

    this._lastThemeStyle = theme;
    this.field.overrideClass(theme);

    this.flushSetCSS();
    this.flushUpdate();

    return true;
  }

  update() {
    this.updateThemeOverride();

    if (this.hasAttribute("datapath")) {
      this.updateDataPath();
    }

    super.update();
  }

  _setDataPath() {
    if (this.hasAttribute("datapath")) {
      let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));

      if (prop === undefined) {
        console.warn("Bad data path for color field:", this.getAttribute("datapath"));
      }

      let val = this.field.rgba;
      if (prop !== undefined && prop.type === PropTypes.VEC3) {
        val = new Vector3();
        val.load(this.field.rgba);
      }

      this.setPathValue(this.ctx, this.getAttribute("datapath"), val);
    }
  }

  getCMYK() {
    return this.field.getCMYK();
  }

  setCMYK(c, m, y, k) {
    this.field.setCMYK(c, m, y, k);
    this._setSliders();
    this._setDataPath();
  }

  setHSVA(h, s, v, a) {
    this.field.setHSVA(h, s, v, a);
    this._setSliders();
    this._setDataPath();
  }

  getRGBA() {
    return this.field.getRGBA();
  }

  setRGBA(r, g, b, a) {
    this.field.setRGBA(r, g, b, a)
    this._setSliders();
    this._setDataPath();
  }
}

UIBase.internalRegister(ColorPicker);


export class ColorPickerButton extends UIBase {
  constructor() {
    super();

    this._highlight = false;
    this._depress = false;
    this._label = "error";

    this.customLabel = undefined;

    this.rgba = new Vector4([1, 1, 1, 1]);

    this.labelDom = document.createElement("span");
    this.labelDom.textContent = this._label;
    this.dom = document.createElement("canvas");
    this.g = this.dom.getContext("2d");

    this.shadow.appendChild(this.labelDom);
    this.shadow.appendChild(this.dom);
  }

  get label() {
    return this._label;
  }

  set label(val) {
    this._label = val;
    this.labelDom.textContent = val;
  }

  get font() {
    return this._font;
  }

  set font(val) {
    this._font = val;

    this.setCSS();
  }

  get noLabel() {
    let ret = "" + this.getAttribute("no-label");
    ret = ret.toLowerCase();

    return ret === "true" || ret === "yes" || ret === "on";
  }

  set noLabel(v) {
    this.setAttribute("no-label", v ? "true" : "false");
  }

  static define() {
    return {
      tagname          : "color-picker-button-x",
      style            : "colorpickerbutton",
      havePickClipboard: true
    }
  }

  init() {
    super.init();
    this._font = "DefaultText"; //this.getDefault("defaultFont");

    let enter = (e) => {
      this._highlight = true;
      this._redraw();
    }

    let leave = (e) => {
      this._highlight = false;
      this._redraw();
    }

    this.addEventListener("pointerover", enter, {capture: true, passive: true});
    this.addEventListener("pointerout", leave, {capture: true, passive: true});
    this.addEventListener("focus", leave, {capture: true, passive: true});


    this.addEventListener("mousedown", (e) => {
      /* ensure browser doesn't spawn its own (incompatible)
         touch->mouse emulation events}; */
      e.preventDefault();

      this.click(e);
    });

    this.setCSS();
  }

  clipboardCopy() {
    colorClipboardCopy.apply(this, arguments);
  }

  clipboardPaste() {
    colorClipboardPaste.apply(this, arguments);
  }

  getRGBA() {
    return this.rgba;
  }

  click(e) {
    this.abortToolTips(4000);
    console.warn("CLICK COLORPICKER");
    this.blur();

    if (this.onclick) {
      this.onclick(e);
    }

    let colorpicker = this.ctx.screen.popup(this, this);
    let ctx = contextWrangler.makeSafeContext(this.ctx);

    colorpicker.ctx = ctx;
    colorpicker.useDataPathUndo = this.useDataPathUndo;

    let path = this.hasAttribute("datapath") ? this.getAttribute("datapath") : undefined;

    let widget = colorpicker.colorPicker(path, undefined, this.getAttribute("mass_set_path"));

    widget.ctx = ctx;
    widget._init();
    widget.setRGBA(this.rgba[0], this.rgba[1], this.rgba[2], this.rgba[3]);

    widget.style["padding"] = "20px";

    let onchange = () => {
      this.rgba.load(widget.rgba);
      this.redraw();

      if (this.onchange) {
        this.onchange(this);
      }
    }

    widget.onchange = onchange;

    colorpicker.style["background-color"] = widget.getDefault("background-color");
    //colorpicker.style["border-radius"] = "25px";
    colorpicker.style["border-width"] = widget.getDefault("border-width");
  }

  setRGBA(val) {
    let a = this.rgba[3];

    let old = new Vector4(this.rgba);

    this.rgba.load(val);

    if (val.length < 4) {
      this.rgba[3] = a;
    }

    if (this.rgba.vectorDistance(old) < 0.001) {
      return;
    }

    if (this.hasAttribute("datapath")) {
      this.setPathValue(this.ctx, this.getAttribute("datapath"), this.rgba);
    }

    if (this.onchange) {
      this.onchange();
    }

    this._redraw();
    return this;
  }

  on_disabled() {
    this.setCSS();
    this._redraw();
  }

  _redraw() {
    let canvas = this.dom, g = this.g;

    g.clearRect(0, 0, canvas.width, canvas.height);

    if (this.disabled) {
      let color = "rgb(55, 55, 55)";

      g.save();

      ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "fill", color);
      ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "clip");
      let steps = 5;
      let dt = canvas.width/steps, t = 0;

      g.beginPath();
      g.lineWidth = 2;
      g.strokeStyle = "black";

      for (let i = 0; i < steps; i++, t += dt) {
        g.moveTo(t, 0);
        g.lineTo(t + dt, canvas.height);
        g.moveTo(t + dt, 0);
        g.lineTo(t, canvas.height);
      }

      g.stroke();
      g.restore();
      return;
    }

    //do checker pattern for alpha
    g.save();

    let grid1 = "rgb(100, 100, 100)";
    let grid2 = "rgb(175, 175, 175)";

    ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "clip");
    ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "fill", grid1);

    let cellsize = 10;
    let totx = Math.ceil(canvas.width/cellsize), toty = Math.ceil(canvas.height/cellsize);

    ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "clip", undefined, undefined, true);
    g.clip();

    g.beginPath();
    for (let x = 0; x < totx; x++) {
      for (let y = 0; y < toty; y++) {
        if ((x + y) & 1) {
          continue;
        }

        g.rect(x*cellsize, y*cellsize, cellsize, cellsize);
      }
    }

    g.fillStyle = grid2;
    g.fill();

    //g.fillStyle = "orange";
    //g.beginPath();
    //g.rect(0, 0, canvas.width, canvas.height);
    //g.fill();

    let color = color2css(this.rgba);
    ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "fill", color, undefined, true);
    //drawRoundBox(elem, canvas, g, width, height, r=undefined, op="fill", color=undefined, pad=undefined) {

    if (this._highlight) {
      //let color = "rgba(200, 200, 255, 0.5)";
      let color = this.getDefault("BoxHighlight");
      ui_base.drawRoundBox(this, canvas, g, canvas.width, canvas.height, undefined, "fill", color);
    }

    g.restore();
  }

  setCSS() {
    super.setCSS();

    let w = this.getDefault("width");
    let h = this.getDefault("height");
    let dpi = this.getDPI();

    this.style["width"] = "min-contents" + "px";
    this.style["height"] = h + "px";

    this.style["flex-direction"] = "row";
    this.style["display"] = "flex";

    this.labelDom.style["color"] = this.getDefault(this._font).color;
    this.labelDom.style["font"] = ui_base.getFont(this, undefined, this._font, false);

    let canvas = this.dom;

    canvas.style["width"] = w + "px";
    canvas.style["height"] = h + "px";
    canvas.width = ~~(w*dpi);
    canvas.height = ~~(h*dpi);

    this.style["background-color"] = "rgba(0,0,0,0)";
    this._redraw();
  }

  updateDataPath() {
    if (!(this.hasAttribute("datapath"))) {
      return;
    }

    let path = this.getAttribute("datapath");
    let prop = this.getPathMeta(this.ctx, path);

    if ((prop === undefined || prop.data === undefined) && cconst.DEBUG.verboseDataPath) {
      console.log("bad path", path);
      return;
    } else if (prop === undefined) {
      let redraw = !this.disabled;

      this.internalDisabled = true;

      if (redraw) {
        this._redraw();
      }
      return;
    }

    let redraw = this.disabled;

    this.internalDisabled = false;

    if (this.customLabel === undefined && prop.uiname !== this._label) {
      this.label = prop.uiname;
    }

    let val = this.getPathValue(this.ctx, path);

    if (val === undefined) {
      redraw = redraw || this.disabled !== true;

      this.internalDisabled = true;

      if (redraw) {
        this._redraw();
      }

    } else {
      this.internalDisabled = false;

      let dis;

      if (val.length === 3) {
        dis = Vector3.prototype.vectorDistance.call(val, this.rgba);
      } else {
        dis = this.rgba.vectorDistance(val);
      }

      if (dis > 0.0001) {
        if (prop.type === PropTypes.VEC3) {
          this.rgba.load(val);
          this.rgba[3] = 1.0;
        } else {
          this.rgba.load(val);
        }

        redraw = true;
      }

      if (redraw) {
        this._redraw();
      }
    }
  }

  update() {
    super.update();

    if (this.noLabel && this.labelDom.isConnected) {
      this.labelDom.remove();
    }

    if (this.customLabel !== undefined && this.customLabel !== this._label) {
      this.label = this.customLabel;
    }

    for (let i = 0; i < this.rgba.length; i++) {
      if (this.rgba[i] === undefined) {
        console.warn("corrupted color or alpha detected", this.rgba);
        this.rgba[i] = 1.0;
      }
    }

    let key = "" + this.rgba[0].toFixed(4) + " " + this.rgba[1].toFixed(4) + " " + this.rgba[2].toFixed(4) + " " + this.rgba[3].toFixed(4);
    key += this.disabled;

    if (key !== this._last_key) {
      this._last_key = key;
      this.redraw();
    }

    if (this.hasAttribute("datapath")) {
      this.updateDataPath();
    }
  }

  redraw() {
    this._redraw();
  }
};
UIBase.internalRegister(ColorPickerButton);