WebGLModule.UIControls.SliderWithInput = class extends WebGLModule.UIControls.IControl {
constructor(context, name, webGLVariableName, params) {
super(context, name, webGLVariableName);
this._c1 = new WebGLModule.UIControls.SimpleUIControl(
context, name, webGLVariableName, params, WebGLModule.UIControls.getUiElement('range'));
params.title = "";
this._c2 = new WebGLModule.UIControls.SimpleUIControl(
context, name, webGLVariableName, params, WebGLModule.UIControls.getUiElement('number'), "second-");
}
init() {
const _this = this;
this._c2._params = this._c1._params;
this._c1.init();
this._c2.init();
this._c1.on("default", function(value, encoded, context) {
$(`#${_this._c2.id}`).val(encoded);
_this._c2.value = value;
_this.changed("default", value, encoded, context);
}, true);
this._c2.on("default", function(value, encoded, context) {
$(`#${_this._c1.id}`).val(encoded);
_this._c1.value = value;
_this.changed("default", value, encoded, context);
}, true);
}
glDrawing(program, dimension, gl) {
this._c1.glDrawing(program, dimension, gl);
}
glLoaded(program, gl) {
this._c1.glLoaded(program, gl);
}
toHtml(breakLine=true, controlCss="") {
if (!this._c1.params.interactive) return "";
let cls = breakLine ? "" : "class='d-inline-block'";
return `<div ${cls} ${controlCss}>${this._c1.toHtml(false, 'width: 48%;')}
${this._c2.toHtml(false, 'width: 12%;')}</div>`;
}
define() {
return this._c1.define();
}
sample(ratio) {
return this._c1.sample(ratio);
}
get supports() {
return this._c1.supports;
}
get params() {
return this._c1.params;
}
get type() {
return this._c1.type;
}
get raw() {
return this._c1.raw;
}
get encoded() {
return this._c1.encoded;
}
};
WebGLModule.UIControls.registerClass("range_input", WebGLModule.UIControls.SliderWithInput);
WebGLModule.UIControls.ColorMap = class extends WebGLModule.UIControls.IControl {
constructor(context, name, webGLVariableName, params) {
super(context, name, webGLVariableName);
this._params = this.getParams(params);
this.prepare();
}
prepare() {
this.MAX_SAMPLES = 8;
this.GLOBAL_GLSL_KEY = 'colormap';
this.parser = WebGLModule.UIControls.getUiElement("color").decode;
if (this.params.continuous) {
this.cssGradient = this._continuousCssFromPallete;
} else {
this.cssGradient = this._discreteCssFromPallete;
}
this.context.includeGlobalCode(this.GLOBAL_GLSL_KEY, this.glslCode());
}
init() {
this.value = this.load(this.params.default);
if (!Array.isArray(this.steps)) this.setSteps();
if (!this.value || !ColorMaps.schemeGroups[this.params.mode].includes(this.value)) {
this.value = ColorMaps.defaults[this.params.mode];
}
this.colorPallete = ColorMaps[this.value][this.maxSteps];
if (this.params.interactive) {
const _this = this;
let updater = function(e) {
let self = $(e.target),
selected = self.val();
_this.colorPallete = ColorMaps[selected][_this.maxSteps];
_this._setPallete(_this.colorPallete);
self.css("background", _this.cssGradient(_this.colorPallete));
_this.value = selected;
_this.store(selected);
_this.changed("default", _this.pallete, _this.value, _this);
_this.context.invalidate();
};
this._setPallete(this.colorPallete);
let node = this.updateColormapUI();
let schemas = [];
for (let pallete of ColorMaps.schemeGroups[this.params.mode]) {
schemas.push(`<option value="${pallete}">${pallete}</option>`);
}
node.html(schemas.join(""));
node.val(this.value);
node.on('change', updater);
} else {
this._setPallete(this.colorPallete);
let node = this.updateColormapUI();
let existsNode = document.getElementById(this.id);
if (existsNode) existsNode.style.background = this.cssGradient(this.pallete);
}
}
glslCode() {
return `
#define COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES} ${this.MAX_SAMPLES}
vec3 sample_colormap(in float ratio, in vec3 map[COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES}], in float steps[COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES}+1], in int max_steps, in bool discrete) {
for (int i = 1; i < COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES} + 1; i++) {
if (ratio <= steps[i]) {
if (discrete) return map[i-1];
float scale = (ratio - steps[i-1]) / (steps[i] - steps[i-1]) - 0.5;
if (scale < .0) {
if (i == 1) return map[0];
//scale should be positive, but we need to keep the right direction
return mix(map[i-1], map[i-2], -scale);
}
if (i == max_steps) return map[i-1];
return mix(map[i-1], map[i], scale);
} else if (i >= max_steps) {
return map[i-1];
}
}
}`
}
updateColormapUI() {
let node = $(`#${this.id}`);
node.css("background", this.cssGradient(this.colorPallete));
return node;
}
setSteps(steps, maximum=this.MAX_SAMPLES) {
this.steps = steps || this.params.steps;
if (! Array.isArray(this.steps)) {
if (this.steps < 2) this.steps = 2;
if (this.steps > maximum) this.steps = maximum;
this.maxSteps = this.steps;
this.steps++;
let step = 1.0 / this.maxSteps;
this.steps = new Array(maximum+1);
this.steps.fill(-1);
this.steps[0] = 0;
for (let i = 1; i < this.maxSteps; i++) this.steps[i] = this.steps[i - 1] + step;
this.steps[this.maxSteps] = 1.0;
} else {
this.steps = this.steps.filter(x => x >= 0);
this.steps.sort();
let max = this.steps[this.steps.length-1];
let min = this.steps[0];
this.steps = this.steps.slice(0, maximum+1);
this.maxSteps = this.steps.length - 1;
this.steps.forEach(x => (x - min) / (max-min));
for (let i = this.maxSteps+1; i < maximum+1; i++) this.steps.push(-1);
}
}
_continuousCssFromPallete(pallete) {
let css = [`linear-gradient(90deg`];
for (let i = 0; i < this.maxSteps; i++) {
css.push(`, ${pallete[i]} ${Math.round((this.steps[i]+this.steps[i+1])*50)}%`);
}
css.push(")");
return css.join("");
}
_discreteCssFromPallete(pallete) {
let css = [`linear-gradient(90deg, ${pallete[0]} 0%`];
for (let i = 1; i < this.maxSteps; i++) {
css.push(`, ${pallete[i-1]} ${Math.round(this.steps[i]*100)}%, ${pallete[i]} ${Math.round(this.steps[i]*100)}%`);
}
css.push(")");
return css.join("");
}
_setPallete(newPallete) {
if (typeof newPallete[0] === "string") {
let temp = newPallete;
this.pallete = [];
for (let color of temp) {
this.pallete.push(...this.parser(color));
}
}
for (let i = this.pallete.length; i < 3*(this.MAX_SAMPLES); i++) this.pallete.push(0);
}
glDrawing(program, dimension, gl) {
gl.uniform3fv(this.colormap_gluint, Float32Array.from(this.pallete));
gl.uniform1fv(this.steps_gluint, Float32Array.from(this.steps));
gl.uniform1i(this.colormap_size_gluint, this.maxSteps);
}
glLoaded(program, gl) {
this.steps_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_steps[0]");
this.colormap_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_colormap[0]");
this.colormap_size_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_colormap_size");
}
toHtml(breakLine=true, controlCss="") {
if (!this.params.interactive) return `<div><span> ${this.params.title}</span><span id="${this.id}" class="text-white-shadow p-1 rounded-2"
style="width: 60%;">${this.load(this.params.default)}</span></div>`;
return `<div><span> ${this.params.title}</span><select id="${this.id}" class="form-control text-white-shadow"
style="width: 60%;"></select></div>`;
}
define() {
return `uniform vec3 ${this.webGLVariableName}_colormap[COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES}];
uniform float ${this.webGLVariableName}_steps[COLORMAP_ARRAY_LEN_${this.MAX_SAMPLES}+1];
uniform int ${this.webGLVariableName}_colormap_size;`;
}
get type() {
return "vec3";
}
sample(value=undefined, valueGlType='void') {
if (!value || valueGlType !== 'float') {
return `ERROR Incompatible control. Colormap cannot be used with ${this.name} (sampling type '${valueGlType}')`;
}
return `sample_colormap(${value}, ${this.webGLVariableName}_colormap, ${this.webGLVariableName}_steps, ${this.webGLVariableName}_colormap_size, ${!this.params.continuous})`;
}
get supports() {
return {
steps: 3,
default: "YlOrRd",
mode: "sequential",
interactive: true,
title: "Colormap",
continuous: false,
};
}
get supportsAll() {
return {
steps: [3, [0, 0.5, 1]]
};
}
get raw() {
return this.pallete;
}
get encoded() {
return this.value;
}
};
WebGLModule.UIControls.registerClass("colormap", WebGLModule.UIControls.ColorMap);
WebGLModule.UIControls.registerClass("custom_colormap", class extends WebGLModule.UIControls.ColorMap {
prepare() {
this.MAX_SAMPLES = 32;
this.GLOBAL_GLSL_KEY = 'custom_colormap';
this.parser = WebGLModule.UIControls.getUiElement("color").decode;
if (this.params.continuous) {
this.cssGradient = this._continuousCssFromPallete;
} else {
this.cssGradient = this._discreteCssFromPallete;
}
this.context.includeGlobalCode(this.GLOBAL_GLSL_KEY, this.glslCode());
}
init() {
this.value = this.load(this.params.default);
if (!Array.isArray(this.steps)) this.setSteps();
if (this.maxSteps < this.value.length) {
this.value = this.value.slice(0, this.maxSteps);
}
this.colorPallete = this.value;
if (this.params.interactive) {
const _this = this;
let updater = function(e) {
let self = $(e.target),
index = Number.parseInt(e.target.dataset.index),
selected = self.val();
if (Number.isInteger(index)) {
_this.colorPallete[index] = selected;
_this._setPallete(_this.colorPallete);
self.parent().css("background", _this.cssGradient(_this.colorPallete));
_this.value = _this.colorPallete;
_this.store(_this.colorPallete);
_this.changed("default", _this.pallete, _this.value, _this);
_this.context.invalidate();
}
};
this._setPallete(this.colorPallete);
let node = this.updateColormapUI();
const width = 1 / this.colorPallete.length * 100;
node.html(this.colorPallete.map((x, i) => `<input type="color" style="width: ${width}%; height: 30px; background: none; border: none; padding: 4px 5px;" value="${x}" data-index="${i}">`).join(""));
node.val(this.value);
node.children().on('change', updater);
} else {
this._setPallete(this.colorPallete);
let node = this.updateColormapUI();
let existsNode = document.getElementById(this.id);
if (existsNode) existsNode.style.background = this.cssGradient(this.pallete);
}
}
toHtml(breakLine=true, controlCss="") {
if (!this.params.interactive) return `<div><span> ${this.params.title}</span><span id="${this.id}" class="text-white-shadow rounded-2 p-0 d-inline-block"
style="width: 60%;"> </span></div>`;
return `<div><span> ${this.params.title}</span><span id="${this.id}" class="form-control text-white-shadow p-0 d-inline-block"
style="width: 60%;"></span></div>`;
}
get supports() {
return {
default: ["#000000", "#888888", "#ffffff"],
steps: 3,
mode: "sequential",
interactive: true,
title: "Colormap:",
continuous: false,
};
}
get supportsAll() {
return {
steps: [3, [0, 0.5, 1]]
};
}
});
WebGLModule.UIControls.AdvancedSlider = class extends WebGLModule.UIControls.IControl {
constructor(context, name, webGLVariableName, params) {
super(context, name, webGLVariableName);
this.MAX_SLIDERS = 12;
this._params = this.getParams(params);
this.context.includeGlobalCode('advanced_slider', `
#define ADVANCED_SLIDER_LEN ${this.MAX_SLIDERS}
float sample_advanced_slider(in float ratio, in float breaks[ADVANCED_SLIDER_LEN], in float mask[ADVANCED_SLIDER_LEN+1], in bool maskOnly, in float minValue) {
float bigger = .0, actualLength = .0, masked = minValue;
bool sampling = true;
for (int i = 0; i < ADVANCED_SLIDER_LEN; i++) {
if (breaks[i] < .0) {
if (sampling) masked = mask[i];
sampling = false;
break;
}
if (sampling) {
if (ratio <= breaks[i]) {
sampling = false;
masked = mask[i];
} else bigger++;
}
actualLength++;
}
if (sampling) masked = mask[ADVANCED_SLIDER_LEN];
if (maskOnly) return masked;
return masked * bigger / actualLength;
}`);
}
init() {
this._updatePending = false;
this.encodedValues = this.load(this.params.breaks, "breaks");
this.mask = this.load(this.params.mask, "mask");
this.value = this.encodedValues.map(this._normalize.bind(this));
this.value = this.value.slice(0, this.MAX_SLIDERS);
this.sampleSize = this.value.length;
this.mask = this.mask.slice(0, this.MAX_SLIDERS+1);
let size = this.mask.length;
this.connects = this.value.map(_ => true); this.connects.push(true);
for (let i = size; i < this.MAX_SLIDERS+1; i++) this.mask.push(-1);
if (!this.params.step || this.params.step < 1) delete this.params.step;
let limit = this.value.length < 2 ? undefined : this.params.max;
let format = this.params.max < 10 ? {
to: v => (v).toLocaleString('en-US', { minimumFractionDigits: 1 }),
from: v => Number.parseFloat(v)
} : {
to: v => (v).toLocaleString('en-US', { minimumFractionDigits: 0 }),
from: v => Number.parseFloat(v)
};
if (this.params.interactive) {
const _this = this;
let container = document.getElementById(this.id);
noUiSlider.create(container, {
range: {
'min': _this.params.min,
'max': _this.params.max
},
step: _this.params.step,
start: _this.encodedValues,
margin: _this.params.minGap,
limit: limit,
connect: _this.connects,
direction: 'ltr',
orientation: 'horizontal',
behaviour: 'drag',
tooltips: true,
format: format,
pips: $.extend({format: format}, this.params.pips)
});
if (this.params.pips) {
let pips = container.querySelectorAll('.noUi-value');
function clickOnPip() {
let idx = 0;
let value = Number(this.getAttribute('data-value'));
let encoded = container.noUiSlider.get();
let values = encoded.map(v => Number.parseFloat(v));
if (Array.isArray(values)) {
let closest = Math.abs(values[0] - value);
for (let i = 1; i < values.length; i++) {
let d = Math.abs(values[i] - value);
if (d < closest) {
idx = i;
closest = d;
}
}
container.noUiSlider.setHandle(idx, value, false, false);
} else {
container.noUiSlider.set(value);
}
value = _this._normalize(value);
_this.value[idx] = value;
_this.changed("breaks", _this.value, encoded, _this);
_this.store(values, "breaks");
_this.context.invalidate();
}
for (let i = 0; i < pips.length; i++) {
pips[i].addEventListener('click', clickOnPip);
}
}
if (this.params.toggleMask) {
this._originalMask = this.mask.map(x => x > 0 ? x : 1);
let connects = container.querySelectorAll('.noUi-connect');
for (let i = 0; i < connects.length; i++) {
connects[i].addEventListener('mouseup', function(e) {
let d = Math.abs(Date.now() - _this._timer);
_this._timer = 0;
if (d >= 180) return;
let idx = Number.parseInt(this.dataset.index);
_this.mask[idx] = _this.mask[idx] > 0 ? 0 : _this._originalMask[idx];
this.style.background = (!_this.params.inverted && _this.mask[idx] > 0)
|| (_this.params.inverted && _this.mask[idx] == 0) ?
"var(--color-icon-danger)" : "var(--color-icon-tertiary)";
_this.context.invalidate();
_this._ignoreNextClick = idx !== 0 && idx !== _this.sampleSize-1;
_this.changed("mask", _this.mask, _this.mask, _this);
_this.store(_this.mask, "mask");
});
connects[i].addEventListener('mousedown', function(e) {
_this._timer = Date.now();
});
connects[i].style.cursor = "pointer";
}
}
container.noUiSlider.on("change", function(strValues, handle, unencoded, tap, positions, noUiSlider) {
_this.value[handle] = _this._normalize(unencoded[handle]);
_this.encodedValues = strValues;
if (_this._ignoreNextClick) {
_this._ignoreNextClick = false;
} else if (!_this._updatePending) {
_this._updatePending = true;
setTimeout(_ => {
_this.changed("breaks", _this.value, strValues, _this);
_this.store(unencoded, "breaks");
_this.context.invalidate();
_this._updatePending = false;
}, 50);
}
});
this._updateConnectStyles(container);
}
for (let i = this.sampleSize; i < this.MAX_SLIDERS; i++) this.value.push(-1);
}
_normalize(value) {
return (value - this.params.min) / (this.params.max - this.params.min);
}
_updateConnectStyles(container) {
if (!container) container = document.getElementById(this.id);
let pips = container.querySelectorAll('.noUi-connect');
for (let i = 0; i < pips.length; i++) {
pips[i].style.background = (!this.params.inverted && this.mask[i] > 0)
|| (this.params.inverted && this.mask[i] == 0) ?
"var(--color-icon-danger)" : "var(--color-icon-tertiary)";
pips[i].dataset.index = (i).toString();
}
}
glDrawing(program, dimension, gl) {
gl.uniform1fv(this.breaks_gluint, Float32Array.from(this.value));
gl.uniform1fv(this.mask_gluint, Float32Array.from(this.mask));
}
glLoaded(program, gl) {
this.min_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_min");
gl.uniform1f(this.min_gluint, this.params.min);
this.breaks_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_breaks[0]");
this.mask_gluint = gl.getUniformLocation(program, this.webGLVariableName + "_mask[0]");
}
toHtml(breakLine=true, controlCss="") {
if (!this.params.interactive) return "";
return `<div><span style="height: 54px;">${this.params.title}: </span><div id="${this.id}" style="height: 9px;
margin-left: 5px; width: 60%; display: inline-block"></div></div>`;
}
define() {
return `uniform float ${this.webGLVariableName}_min;
uniform float ${this.webGLVariableName}_breaks[ADVANCED_SLIDER_LEN];
uniform float ${this.webGLVariableName}_mask[ADVANCED_SLIDER_LEN+1];`;
}
get type() {
return "float";
}
sample(value=undefined, valueGlType='void') {
if (!value || valueGlType !== 'float') {
return `ERROR Incompatible control. Advanced slider cannot be used with ${this.name} (sampling type '${valueGlType}')`;
}
return `sample_advanced_slider(${value}, ${this.webGLVariableName}_breaks, ${this.webGLVariableName}_mask, ${this.params.maskOnly}, ${this.webGLVariableName}_min)`;
}
get supports() {
return {
breaks: [0.2, 0.8],
mask: [1, 0, 1],
interactive: true,
inverted: true,
maskOnly: true,
toggleMask: true,
title: "Threshold",
min: 0,
max: 1,
minGap: 0.05,
step: null,
pips: {
mode: 'positions',
values: [0, 20, 40, 50, 60, 80, 90, 100],
density: 4
}
};
}
get supportsAll() {
return {
step: [null, 0.1]
};
}
get raw() {
return this.value;
}
get encoded() {
return this.encodedValues;
}
};
WebGLModule.UIControls.registerClass("advanced_slider", WebGLModule.UIControls.AdvancedSlider);
WebGLModule.UIControls.TextArea = class extends WebGLModule.UIControls.IControl {
constructor(context, name, webGLVariableName, params) {
super(context, name, webGLVariableName);
this._params = this.getParams(params);
}
init() {
this.value = this.load(this.params.default);
if (this.params.interactive) {
const _this = this;
let updater = function(e) {
let self = $(e.target);
_this.value = self.val();
_this.store(_this.value);
_this.changed("default", _this.value, _this.value, _this);
};
let node = $(`#${this.id}`);
node.val(this.value);
node.on('change', updater);
} else {
let node = $(`#${this.id}`);
node.val(this.value);
}
}
glDrawing(program, dimension, gl) {
}
glLoaded(program, gl) {
}
toHtml(breakLine=true, controlCss="") {
let disabled = this.params.interactive ? "" : "disabled";
let title = this.params.title ? `<span style="height: 54px;">${this.params.title}: </span>` : "";
return `<div>${title}<textarea id="${this.id}" class="form-control"
style="width: 100%; display: block; resize: vertical; ${controlCss}" ${disabled} placeholder="${this.params.placeholder}"></textarea></div>`;
}
define() {
return "";
}
get type() {
return "text";
}
sample(value=undefined, valueGlType='void') {
return this.value;
}
get supports() {
return {
default: "",
placeholder: "",
interactive: true,
title: "Text"
};
}
get supportsAll() {
return {};
}
get raw() {
return this.value;
}
get encoded() {
return this.value;
}
};
WebGLModule.UIControls.registerClass("text_area", WebGLModule.UIControls.TextArea);
WebGLModule.UIControls.Button = class extends WebGLModule.UIControls.IControl {
constructor(context, name, webGLVariableName, params) {
super(context, name, webGLVariableName);
this._params = this.getParams(params);
}
init() {
this.value = this.load(this.params.default);
if (this.params.interactive) {
const _this = this;
let updater = function(e) {
_this.value++;
_this.store(_this.value);
_this.changed("default", _this.value, _this.value, _this);
};
let node = $(`#${this.id}`);
node.html(this.params.title);
node.click(updater);
} else {
let node = $(`#${this.id}`);
node.html(this.params.title);
}
}
glDrawing(program, dimension, gl) {
}
glLoaded(program, gl) {
}
toHtml(breakLine=true, controlCss="") {
let disabled = this.params.interactive ? "" : "disabled";
let css = `style="${controlCss ? controlCss : ""}float: right;"`;
return `<button id="${this.id}" ${css} class="btn" ${disabled}></button>
${breakLine ? '<br style="clear: both;">' : ""}`;
}
define() {
return "";
}
get type() {
return "action";
}
sample(value=undefined, valueGlType='void') {
return "";
}
get supports() {
return {
default: 0,
interactive: true,
title: "Button"
};
}
get supportsAll() {
return {};
}
get raw() {
return this.value;
}
get encoded() {
return this.value;
}
};
WebGLModule.UIControls.registerClass("button", WebGLModule.UIControls.Button);