/**
* Shader sharing point
* @class WebGLModule.ShaderMediator
*/
WebGLModule.ShaderMediator = class {
static _layers = {};
/**
* Register shader
* @param {function} LayerRendererClass class extends WebGLModule.VisualizationLayer
*/
static registerLayer(LayerRendererClass) {
if (this._layers.hasOwnProperty(LayerRendererClass.type())) {
console.warn("Registering an already existing layer renderer:", LayerRendererClass.type());
}
if (!WebGLModule.VisualizationLayer.isPrototypeOf(LayerRendererClass)) {
throw `${LayerRendererClass} does not inherit from VisualizationLayer!`;
}
this._layers[LayerRendererClass.type()] = LayerRendererClass;
}
/**
* Get the shader class by type id
* @param {string} id
* @return {function} class extends WebGLModule.VisualizationLayer
*/
static getClass(id) {
return this._layers[id];
}
/**
* Get all available shaders
* @return {function[]} classes that extend WebGLModule.VisualizationLayer
*/
static availableShaders() {
return Object.values(this._layers);
}
};
/**
* Abstract interface to any Shader.
* @abstract
*/
WebGLModule.VisualizationLayer = class {
/**
* Override **static** type definition
* The class must be registered using the type
* @returns {string} unique id under which is the shader registered
*/
static type() {
throw "VisualizationLayer::type() Type must be specified!";
}
/**
* Override **static** name definition
* @returns {string} name of the shader (user-friendly)
*/
static name() {
throw "VisualizationLayer::name() Name must be specified!";
}
/**
* Provide description
* @returns {string} optional description
*/
static description() {
return "VisualizationLayer::description() WebGL shader must provide description.";
}
/**
* Default preview image URL getter,
* override if your image is not stored in webgl/shaders/[id].png
* Remove XOpatModule.ROOT reference if you do not use XOpat API
*/
static preview(moduleRootDir=XOpatModule.ROOT) {
return moduleRootDir + "webgl/shaders/" + this.type() + ".png";
}
/**
* Declare the number of data sources it reads from (how many dataSources indexes should the shader contain)
* @return {Array.<Object>} array of source specifications:
* acceptsChannelCount: predicate that evaluates whether given number of channels (argument) is acceptable
* [optional] description: the description of the source - what it is being used for
*/
static sources() {
throw "VisualizationLayer::sources() Shader must specify channel acceptance predicates for each source it uses!";
}
/**
* Declare supported controls by a particular shader
* each controls is automatically created for the shader
* and this[controlId] instance set
* structure:
* {
* controlId: {
default: {type: <>, title: <>, interactive: true|false...},
accepts: (type, instance) => <>,
required: {type: <> ...} [OPTIONAL]
* }, ...
* }
*
* use [controlId]: false to disable a specific control (e.g. all shaders
* support opacity by default - use to remove this feature)
*
* Additionally, use_[...] value can be specified, such controls enable shader
* to specify default or required values for built-in use_[...] params. example:
* {
* use_channel0: {
* default: "bg"
* },
* use_channel1: {
* required: "rg"
* },
* use_gamma: {
* default: 0.5
* }
* }
* reads by default for texture 1 channels 'bg', second texture is always forced to read 'rg',
* textures apply gamma filter with 0.5 by default if not overridden
* todo: allow also custom object without structure being specified (use in custom manner,
* but limited in automated docs --> require field that summarises its usage)
* @member {object}
*/
static defaultControls = {};
/**
* Declare custom parameters for documentation purposes.
* Can set default values to provide sensible defaults.
* Requires only 'usage' parameter describing the use.
* Unlike controls, these values are not processed in any way.
* Of course you don't have to define your custom parameters,
* but then these won't be documented in any nice way. Note that
* the value can be an object, or a different value (e.g., an array)
* {
* customParamId: {
* default: {myItem: 1, myValue: "string" ...}, [OPTIONAL]
* usage: "This parameter can be used like this and that.",
* required: {type: <> ...} [OPTIONAL]
* }, ...
* }
* @type {any}
*/
static customParams = {};
/**
* Global supported options
* @param {string} id unique ID among all webgl instances and shaders
* @param {object} privateOptions options that should not be touched, necessary for linking the layer to the core
*/
constructor(id, privateOptions) {
this.uid = id;
if (!WebGLModule.idPattern.test(this.uid)) {
console.error("Invalid ID for the shader: id must match to the pattern", WebGLModule.idPattern, id);
}
this._setContextVisualizationLayer(privateOptions.layer);
//todo custom control names share namespace with this API - unique names or controls in seperate object?
this.webglContext = privateOptions.webgl;
this.invalidate = privateOptions.invalidate;
//use with care... todo document
this._rebuild = privateOptions.rebuild;
this._refetch = privateOptions.refetch;
this._hasInteractiveControls = privateOptions.interactive;
}
/**
* Manual constructor, must call super.construct(...) if overridden, but unlike
* constructor the call can be adjusted (e.g. adjust option values)
* @param {WebGLModule.ShaderLayerParams} options
* options.use_channel[X]: "r", "g" or "b" channel to sample index X, default "r"
* options.use_mode: blending mode - default alpha ("show"), custom blending ("mask") and clipping mask blend ("mask_clip")
* options.use_[*]: filtering, gamma/exposure/logscale with a float filter parameter (e.g. "use_gamma" : 1.5)
* @param {Array<number>} dataReferences indexes of data being requested for this shader
*/
construct(options, dataReferences) {
this._ownedControls = [];
this._buildControls(options);
this.resetChannel(options);
this.resetMode(options);
this.resetFilters(options);
}
/**
* Code placed outside fragment shader's main(...).
* By default, it includes all definitions of
* controls you defined in defaultControls
*
* NOTE THAT ANY VARIABLE NAME
* WITHIN THE GLOBAL SPACE MUST BE
* ESCAPED WITH UNIQUE ID: this.uid
*
* DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code
*
* WHEN OVERRIDING, INCLUDE THE OUTPUT OF THIS METHOD AT THE BEGINNING OF THE NEW OUTPUT.
*
* @return {string}
*/
getFragmentShaderDefinition() {
let html = [];
for (let control of this._ownedControls) {
let code = this[control].define()?.trim();
if (code) html.push(code);
}
return html.join("\n");
}
/**
* Code executed to create the output color. The code
* must always return a vec4 value, otherwise the visualization
* will fail to compile (this code actually runs inside a vec4 function).
*
* DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code
*
* @return {string}
*/
getFragmentShaderExecution() {
throw "VisualizationLayer::getFragmentShaderExecution must be implemented!";
}
/**
* Called when an image is rendered
* @param {WebGLProgram} program WebglProgram instance
* @param {object} dimension canvas dimension
* @param {number} dimension.width
* @param {number} dimension.height
* @param {WebGLRenderingContext|WebGL2RenderingContext} gl
*/
glDrawing(program, dimension, gl) {
for (let control of this._ownedControls) {
this[control].glDrawing(program, dimension, gl);
}
}
/**
* Called when associated webgl program is switched to
* @param {WebGLProgram} program WebglProgram instance
* @param {WebGLRenderingContext|WebGL2RenderingContext} gl WebGL Context
*/
glLoaded(program, gl) {
for (let control of this._ownedControls) {
this[control].glLoaded(program, gl);
}
}
/**
* This function is called once at
* the beginning of the layer use
* (might be multiple times), after htmlControls()
*/
init() {
if (!this.initialized()) {
console.error("Shader not properly initialized! Call shader.construct()!");
}
for (let control of this._ownedControls) {
this[control].init();
}
}
/**
* Get the shader UI controls
* @return {string} HTML controls for the particular shader
*/
htmlControls() {
let html = [];
for (let control of this._ownedControls) {
if (this.hasOwnProperty(control)) {
html.push(this[control].toHtml(true));
}
}
return html.join("");
}
/**
* @param id control id to delete
*/
removeControl(id) {
if (!this._ownedControls[id]) return;
delete this._ownedControls[id];
delete this[id];
}
/**
* Check if shader is initialized.
* @return {boolean}
*/
initialized() {
return !!this._ownedControls;
}
/************************** FILTERING ****************************/
//not really modular
//add your filters here if you want... function that takes parameter (number)
//and returns prefix and suffix to compute oneliner filter
//should start as 'use_[name]' for namespace collision avoidance (params object)
//expression should be wrapped in parenthesses for safety: ["(....(", ")....)"] in the middle the
// filtered variable will be inserted, notice pow does not need inner brackets since its an argument...
//note: pow avoided in gamma, not usable on vectors, we use pow(x, y) === exp(y*log(x))
// TODO: implement filters as shader nodes instead!
static filters = {
use_gamma: (x) => ["exp(log(", `) / ${this.toShaderFloatString(x, 1)})`],
use_exposure: (x) => ["(1.0 - exp(-(", `)* ${this.toShaderFloatString(x, 1)}))`],
use_logscale: (x) => {
x = this.toShaderFloatString(x, 1);
return [`((log(${x} + (`, `)) - log(${x})) / (log(${x}+1.0)-log(${x})))`]
}
};
/**
* Available filters (use_[name])
* @type {{use_exposure: string, use_gamma: string, use_logscale: string}}
*/
static filterNames = {
use_gamma: "Gamma",
use_exposure: "Exposure",
use_logscale: "Logarithmic scale"
};
/**
* Available use_mode modes
* @type {{show: string, mask: string}}
*/
static modes = {
show: "show",
mask: "blend",
mask_clip: "blend_clip"
};
/**
* Include GLSL shader code on global scope
* (e.g. define function that is repeatedly used)
* does not have to use unique ID extended names as this code is included only once
* @param {string} key a key under which is the code stored, so that the same key is not loaded twice
* @param {string} code GLSL code to add to the shader
*/
includeGlobalCode(key, code) {
let container = this.constructor.__globalIncludes;
if (!container.hasOwnProperty(key)) container[key] = code;
}
/**
* Parses value to a float string representation with given precision (length after decimal)
* @param {number} value value to convert
* @param {number} defaultValue default value on failure
* @param {number} precisionLen number of decimals
* @return {string}
*/
toShaderFloatString(value, defaultValue, precisionLen=5) {
return this.constructor.toShaderFloatString(value, defaultValue, precisionLen);
}
/**
* Parses value to a float string representation with given precision (length after decimal)
* @param {number} value value to convert
* @param {number} defaultValue default value on failure
* @param {number} precisionLen number of decimals
* @return {string}
*/
static toShaderFloatString(value, defaultValue, precisionLen=5) {
if (!Number.isInteger(precisionLen) || precisionLen < 0 || precisionLen > 9) {
precisionLen = 5;
}
try {
return value.toFixed(precisionLen);
} catch (e) {
return defaultValue.toFixed(precisionLen);
}
}
/**
* Apply global filters on value
* @param {string} value GLSL code string, value to filter
* @return {string} filtered value (GLSL oneliner without ';')
*/
filter(value) {
return `${this.__scalePrefix}${value}${this.__scaleSuffix}`;
}
/**
* Sample only one channel (which is defined in options)
* @param {string} textureCoords valid GLSL vec2 object as string
* @param {number} otherDataIndex index of the data in self.dataReference JSON array
* @param {boolean} raw whether to output raw value from the texture (do not apply filters)
* @return {string} code for appropriate texture sampling within the shader,
* where only one channel is extracted or float with zero value if
* the reference is not valid
*/
sampleChannel(textureCoords, otherDataIndex=0, raw=false) {
let refs = this.__visualizationLayer.dataReferences;
const chan = this.__channels[otherDataIndex];
if (otherDataIndex >= refs.length) {
switch (chan.length) {
case 1: return ".0";
case 2: return "vec2(.0)";
case 3: return "vec3(.0)";
default:
return 'vec4(0.0)';
}
}
let sampled = `${this.webglContext.getTextureSamplingCode(refs[otherDataIndex], textureCoords)}.${chan}`;
if (raw) return sampled;
return this.filter(sampled);
}
/**
* Get texture size
* @param {number} index index of the data in self.dataReference JSON array
* @return {string} vec2 GLSL value with width and height of the texture
*/
textureSize(index=0) {
let refs = this.__visualizationLayer.dataReferences;
if (index >= refs.length) {
return 'vec2(0.0)';
}
return this.webglContext.getTextureDimensionXY(refs[index]);
}
/**
* For error detection, how many textures are available
* @return {number} number of textures available
*/
dataSourcesCount() {
return this.__visualizationLayer.dataReferences.length;
}
/**
* Load value, useful for controls value caching
* @param {string} name value name
* @param {string} defaultValue default value if no stored value available
* @return {string} stored value or default value
*/
loadProperty(name, defaultValue) {
let selfType = this.constructor.type();
if (!this.__visualizationLayer) return defaultValue;
if (this.__visualizationLayer.cache[selfType].hasOwnProperty(name)) {
return this.__visualizationLayer.cache[selfType][name];
}
return defaultValue;
}
/**
* Store value, useful for controls value caching
* @param {string} name value name
* @param {*} value value
*/
storeProperty(name, value) {
this.__visualizationLayer.cache[this.constructor.type()][name] = value;
}
/**
* Evaluates option flag, e.g. any value that indicates boolean 'true'
* @param {*} value value to interpret
* @return {boolean} true if the value is considered boolean 'true'
*/
isFlag(value) {
return value == "1" || value == true || value == "true";
}
isFlagOrMissing(value) {
return value === undefined || this.isFlag(value);
}
/**
* Get the mode we operate in
* @return {string} mode
*/
get mode() {
return this._mode;
}
/**
* Returns number of textures available to this shader
* @return {number} number of textures available
*/
get texturesCount() {
return this.__visualizationLayer.dataReferences.length;
}
/**
* Set filter value
* @param filter filter name
* @param value value of the filter
*/
setFilterValue(filter, value) {
if (!this.constructor.filterNames.hasOwnProperty(filter)) {
console.error("Invalid filter name.", filter);
return;
}
this.storeProperty(filter, value);
}
/**
* Get the filter value (alias for loadProperty(...)
* @param {string} filter filter to read the value of
* @param {string} defaultValue
* @return {string} stored filter value or defaultValue if no value available
*/
getFilterValue(filter, defaultValue) {
return this.loadProperty(filter, defaultValue);
}
/**
* Set sampling channel
* @param {object} options
* @param {string} options.use_channel[X] chanel swizzling definition to sample
*/
resetChannel(options) {
const parseChannel = (name, def, sourceDef) => {
const predefined = this.constructor.defaultControls[name];
if (options.hasOwnProperty(name) || predefined) {
let channel = predefined?.required || this.loadProperty(name, options[name]) || predefined?.default;
if (!channel
|| typeof channel !== "string"
|| this.constructor.__chanPattern.exec(channel) === null) {
console.warn(`Invalid channel '${name}'. Will use channel '${def}'.`, channel, options);
this.storeProperty(name, "r");
channel = def;
}
if (!sourceDef.acceptsChannelCount(channel.length)) {
throw `${this.constructor.name()} does not support channel length for channel: ${channel}`;
}
if (channel !== options[name]) this.storeProperty(name, channel);
return channel;
}
return def;
};
this.__channels = this.constructor.sources().map((source, i) => parseChannel(`use_channel${i}`, "r", source));
}
/**
* Set blending mode
* @param {object} options
* @param {string} options.use_mode blending mode to use: "show" or "mask"
*/
resetMode(options) {
const predefined = this.constructor.defaultControls.use_mode;
if (options.hasOwnProperty("use_mode")) {
this._mode = predefined?.required || this.loadProperty("use_mode", options.use_mode);
if (this._mode !== options.use_mode) this.storeProperty("use_mode", this._mode);
} else {
this._mode = predefined?.default || "show";
}
this.__mode = this.constructor.modes[this._mode] || "show";
}
/**
* Can be used to re-set filters for a shader
* @param {object} options filters configuration, currently supported are
* 'use_gamma', 'use_exposure', 'use_logscale'
*/
resetFilters(options) {
this.__scalePrefix = [];
this.__scaleSuffix = [];
let THIS = this.constructor;
for (let key in THIS.filters) {
const predefined = this.constructor.defaultControls[key];
let value = predefined?.required;
if (value === undefined) {
if (options.hasOwnProperty(key)) value = this.loadProperty(key, options[key]);
else value = predefined?.default;
}
if (value !== undefined) {
let filter = THIS.filters[key](value);
this.__scalePrefix.push(filter[0]);
this.__scaleSuffix.push(filter[1]);
}
}
this.__scalePrefix = this.__scalePrefix.join("");
this.__scaleSuffix = this.__scaleSuffix.reverse().join("");
}
/**
*
* @param name the control named ID which will be attached to the control
* @param {object} controlOptions control options defined by the underlying
* control, must have at least 'type' property
* @param {object} buildContext item with the same properties
* as static.defaultControls
*/
addControl(name, controlOptions, buildContext) {
if (this.hasOwnProperty(name)) {
console.warn(`Shader ${this.constructor.name()} overrides as a control name ${name} existing property!`);
}
this._ownedControls.push(name);
const control = WebGLModule.UIControls.build(this, name, controlOptions,
buildContext.default, buildContext.accepts, buildContext.required, this._hasInteractiveControls);
this[name] = control;
return control;
}
////////////////////////////////////
////////// PRIVATE /////////////////
////////////////////////////////////
static __globalIncludes = {};
static __chanPattern = new RegExp('[rgba]{1,4}');
_buildControls(options) {
let controls = this.constructor.defaultControls,
customParams = this.constructor.customParams;
if (controls.opacity === undefined || (typeof controls.opacity === "object" && !controls.opacity.accepts("float"))) {
controls.opacity = {
default: {type: "range", default: 1, min: 0, max: 1, step: 0.1, title: "Opacity: "},
accepts: (type, instance) => type === "float"
};
}
for (let control in controls) {
if (controls.hasOwnProperty(control)) {
if (control.startsWith("use_")) continue;
let buildContext = controls[control];
if (buildContext) {
this.addControl(control, options[control], buildContext);
continue;
}
let customContext = customParams[control];
if (customContext) {
let targetType;
const dType = typeof customContext.default, rType = typeof customContext.required;
if (dType !== rType) console.error("Custom parameters for shader do not match!",
dType, rType, this.constructor.name());
if (rType !== 'undefined') targetType = rType;
else if (dType !== 'undefined') targetType = dType;
else if (dType !== 'undefined') targetType = dType;
else targetType = 'object';
if (targetType === 'object') {
let knownOptions = options[control];
if (!knownOptions) knownOptions = options[control] = {};
if (customContext.default) $.extend(knownOptions, customContext.default);
if (options[control]) $.extend(knownOptions, options[control]);
if (customContext.required) $.extend(knownOptions, customContext.required);
} else {
if (customContext.required !== undefined) options[control] = customContext.required;
else if (options[control] === undefined) options[control] = customContext.default;
}
}
}
}
}
_setContextVisualizationLayer(visualizationLayer) {
this.__visualizationLayer = visualizationLayer;
if (!this.__visualizationLayer.hasOwnProperty("cache")) this.__visualizationLayer.cache = {};
if (!this.__visualizationLayer.cache.hasOwnProperty(this.constructor.type())) {
this.__visualizationLayer.cache[this.constructor.type()] = {};
}
}
};
/**
* Factory Manager for predefined UIControls
* - you can manage all your UI control logic within your shader implementation
* and not to touch this class at all, but here you will find some most common
* or some advanced controls ready to use, simple and powerful
* - registering an IComponent implementation (or an UiElement) in the factory results in its support
* among all the shaders (given the GLSL type, result of sample(...) matches).
* - UiElements are objects to create simple controls quickly and get rid of code duplicity,
* for more info @see WebGLModule.UIControls.register()
* @class WebGLModule.UIControls
*/
WebGLModule.UIControls = class {
/**
* Get all available control types
* @return {string[]} array of available control types
*/
static types() {
return Object.keys(this._items).concat(Object.keys(this._impls));
}
/**
* Get an element used to create simple controls, if you want
* an implementation of the controls themselves (IControl), use build(...) to instantiate
* @param {string} id type of the control
* @return {*}
*/
static getUiElement(id) {
let ctrl = this._items[id];
if (!ctrl) {
console.error("Invalid control: " + id);
ctrl = this._items["number"];
}
return ctrl;
}
/**
* Get an element used to create advanced controls, if you want
* an implementation of simple controls, use build(...) to instantiate
* @param {string} id type of the control
* @return {WebGLModule.UIControls.IControl}
*/
static getUiClass(id) {
let ctrl = this._impls[id];
if (!ctrl) {
console.error("Invalid control: " + id);
ctrl = this._impls["colormap"];
}
return ctrl;
}
/**
* Build UI control object based on given parameters
* @param {WebGLModule.VisualizationLayer} context owner of the control
* @param {string} name name used for the layer, should be unique among different context types
* @param {object|*} params parameters passed to the control (defined by the control) or set as default value if not object
* @param {object} defaultParams default parameters that the shader might leverage above defaults of the control itself
* @param {function} accepts required GLSL type of the control predicate, for compatibility typechecking
* @param {object} requiredParams parameters that override anything sent by user or present by defaultParams
* @param {boolean} interactivityEnabled must be false if HTML nodes are not managed
* @return {WebGLModule.UIControls.IControl}
*/
static build(context, name, params, defaultParams={}, accepts=() => true, requiredParams={}, interactivityEnabled=true) {
//if not an object, but a value: make it the default one
if (!(typeof params === 'object')) {
params = {default: params};
}
if (!interactivityEnabled) {
params.interactive = false;
}
let originalType = defaultParams.type;
defaultParams = $.extend(true, {}, defaultParams, params, requiredParams);
if (!this._items.hasOwnProperty(defaultParams.type)) {
if (!this._impls.hasOwnProperty(defaultParams.type)) {
return this._buildFallback(defaultParams.type, originalType, context,
name, params, defaultParams, accepts, requiredParams, interactivityEnabled);
}
let cls = new this._impls[defaultParams.type](
context, name, `${name}_${context.uid}`, defaultParams
);
if (accepts(cls.type, cls)) return cls;
return this._buildFallback(defaultParams.type, originalType, context,
name, params, defaultParams, accepts, requiredParams, interactivityEnabled);
} else {
let contextComponent = this.getUiElement(defaultParams.type);
let comp = new WebGLModule.UIControls.SimpleUIControl(
context, name, `${name}_${context.uid}`, defaultParams, contextComponent
);
if (accepts(comp.type, comp)) return comp;
return this._buildFallback(contextComponent.glType, originalType, context,
name, params, defaultParams, accepts, requiredParams, interactivityEnabled);
}
}
/**
* Register simple UI element by providing necessary object
* implementation:
* { defaults: function() {...}, // object with all default values for all supported parameters
html: function(uniqueId, params, css="") {...}, //how the HTML UI controls look like
glUniformFunName: function() {...}, //what function webGL uses to pass this attribute to GPU
decode: function(fromValue) {...}, //parse value obtained from HTML controls into something
gl[glUniformFunName()](...) can pass to GPU
glType: //what's the type of this parameter wrt. GLSL: int? vec3?
* @param type the identifier under which is this control used: lookup made against params.type
* @param uiElement the object to register, fulfilling the above-described contract
*/
static register(type, uiElement) {
function check(el, prop, desc) {
if (!el.hasOwnProperty(prop)) {
console.warn(`Skipping UI control '${type}' due to '${prop}': missing ${desc}.`);
return false;
}
return true;
}
if (check(uiElement, "defaults", "defaults():object")
&& check(uiElement, "html", "html(uniqueId, params, css):htmlString")
&& check(uiElement, "glUniformFunName", "glUniformFunName():string")
&& check(uiElement, "decode", "decode(encodedValue):<compatible with glType>")
&& check(uiElement, "normalize", "normalize(value, params):<typeof value>")
&& check(uiElement, "sample", "sample(value, valueGlType):glslString")
&& check(uiElement, "glType", "glType:string")
) {
uiElement.prototype.getName = () => type;
if (this._items.hasOwnProperty(type)) {
console.warn("Registering an already existing control component: ", type);
}
uiElement["uiType"] = type;
this._items[type] = uiElement;
}
}
/**
* Register class as a UI control
* @param {string} type unique control name / identifier
* @param {WebGLModule.UIControls.IControl} cls to register, implementation class of the controls
*/
static registerClass(type, cls) {
if (WebGLModule.UIControls.IControl.isPrototypeOf(cls)) {
cls.prototype.getName = () => type;
if (this._items.hasOwnProperty(type)) {
console.warn("Registering an already existing control component: ", type);
}
cls._uiType = type;
this._impls[type] = cls;
} else {
console.warn(`Skipping UI control '${type}': does not inherit from WebGLModule.UIControls.IControl.`);
}
}
/////////////////////////
/////// PRIVATE /////////
/////////////////////////
//implementation of UI control classes
//more complex functionality
static _impls = {
//colormap: WebGLModule.UIControls.ColorMap
};
//implementation of UI control objects
//simple functionality
static _items = {
number: {
defaults: function() {
return {title: "Number", interactive: true, default: 0, min: 0, max: 100, step: 1};
},
html: function(uniqueId, params, css="") {
let title = params.title ? `<span> ${params.title}</span>` : "";
return `${title}<input class="form-control input-sm" style="${css}" min="${params.min}" max="${params.max}"
step="${params.step}" type="number" id="${uniqueId}">`;
},
glUniformFunName: function() {
return "uniform1f";
},
decode: function(fromValue) {
return Number.parseFloat(fromValue);
},
normalize: function(value, params) {
return (value - params.min) / (params.max - params.min);
},
sample: function(name, ratio) {
return name;
},
glType: "float",
uiType: "number"
},
range: {
defaults: function() {
return {title: "Range", interactive: true, default: 0, min: 0, max: 100, step: 1};
},
html: function(uniqueId, params, css="") {
let title = params.title ? `<span> ${params.title}</span>` : "";
return `${title}<input type="range" style="${css}"
class="with-direct-input" min="${params.min}" max="${params.max}" step="${params.step}" id="${uniqueId}">`;
},
glUniformFunName: function() {
return "uniform1f";
},
decode: function(fromValue) {
return Number.parseFloat(fromValue);
},
normalize: function(value, params) {
return (value - params.min) / (params.max - params.min);
},
sample: function(name, ratio) {
return name;
},
glType: "float",
uiType: "range"
},
color: {
defaults: function() {
return { title: "Color", interactive: true, default: "#fff900" };
},
html: function(uniqueId, params, css="") {
let title = params.title ? `<span> ${params.title}</span>` : "";
return `${title}<input type="color" id="${uniqueId}" style="${css}" class="form-control input-sm">`;
},
glUniformFunName: function() {
return "uniform3fv";
},
decode: function(fromValue) {
try {
let index = fromValue.startsWith("#") ? 1 : 0;
return [
parseInt(fromValue.slice(index, index+2), 16) / 255,
parseInt(fromValue.slice(index+2, index+4), 16) / 255,
parseInt(fromValue.slice(index+4, index+6), 16) / 255
];
} catch (e) {
return [0, 0, 0];
}
},
normalize: function(value, params) {
return value;
},
sample: function(name, ratio) {
return name;
},
glType: "vec3",
uiType: "color"
},
bool: {
defaults: function() {
return { title: "Checkbox", interactive: true, default: true };
},
html: function(uniqueId, params, css="") {
let title = params.title ? `<span> ${params.title}</span>` : "";
let value = this.decode(params.default) ? "checked" : "";
//note a bit dirty, but works :) - we want uniform access to 'value' property of all inputs
return `${title}<input type="checkbox" style="${css}" id="${uniqueId}" ${value}
class="form-control input-sm" onchange="this.value=this.checked; return true;">`;
},
glUniformFunName: function() {
return "uniform1i";
},
decode: function(fromValue) {
return fromValue && fromValue !== "false" ? 1 : 0;
},
normalize: function(value, params) {
return value;
},
sample: function(name, ratio) {
return name;
},
glType: "bool",
uiType: "bool"
}
};
static _buildFallback(newType, originalType, context, name, params, defaultParams,
requiredType, requiredParams, interactivityEnabled) {
//repeated check when building object from type
params.interactive = false;
if (originalType === newType) { //if default and new equal, fail - recursion will not help
console.error(`Invalid parameter in shader '${params.type}': the parameter could not be built.`);
return undefined;
} else { //otherwise try to build with originalType (default)
params.type = originalType;
console.warn("Incompatible UI control type '"+newType+"': making the input non-interactive.");
return this.build(context, name, params, defaultParams, requiredType, requiredParams, interactivityEnabled);
}
}
};
/**
* @interface
*/
WebGLModule.UIControls.IControl = class {
/**
* Sets common properties needed to create the controls:
* this.context @extends WebGLModule.VisualizationLayer - owner context
* this.name - name of the parameter for this.context.[load/store]Property(...) call
* this.id - unique ID for HTML id attribute, to be able to locate controls in DOM,
* created as ${uniq}${name}-${context.uid}
* this.webGLVariableName - unique webgl uniform variable name, to not to cause conflicts
*
* If extended (class-based definition, see registerCass) children should define constructor as
*
* @example
* constructor(context, name, webGLVariableName, params) {
* super(context, name, webGLVariableName);
* ...
* //possibly make use of params:
* this.params = this.getParams(params);
*
* //now access params:
* this.params...
* }
*
* @param {WebGLModule.VisualizationLayer} context shader context owning this control
* @param {string} name name of the control (key to the params in the shader configuration)
* @param {string} webGLVariableName configuration parameters,
* depending on the params.type field (the only one required)
* @param {string} uniq another element to construct the DOM id from, mostly for compound controls
*/
constructor(context, name, webGLVariableName, uniq="") {
this.context = context;
this.id = `${uniq}${name}-${context.uid}`;
this.name = name;
this.webGLVariableName = webGLVariableName;
this._params = {};
}
/**
* Safely sets outer params with extension from 'supports'
* - overrides 'supports' values with the correct type (derived from supports or supportsAll)
* - sets 'supports' as defaults if not set
* @param params
*/
getParams(params) {
const t = this.constructor.getVarType;
function mergeSafeType(mask, from, possibleTypes) {
const to = {...mask};
Object.keys(from).forEach(key => {
const tVal = to[key],
fVal = from[key],
tType = t(tVal),
fType = t(fVal);
const typeList = possibleTypes?.[key],
pTypeList = typeList ? typeList.map(x => t(x)) : [];
//our type detector distinguishes arrays and objects
if (tVal && fVal && tType === "object" && fType === "object") {
to[key] = mergeSafeType(tVal, fVal, typeList);
} else if (tVal === undefined || tType === fType || pTypeList.includes(fType)) {
to[key] = fVal;
} else if (fType === "string") {
//try parsing NOTE: parsing from supportsAll is ignored!
if (tType === "number") {
const parsed = Number.parseFloat(fVal);
if (!Number.isNaN(parsed)) to[key] = parsed;
} else if (tType === "boolean") {
const value = fVal.toLowerCase();
if (value === "false") to[key] = false;
if (value === "true") to[key] = true;
}
}
});
return to;
}
return mergeSafeType(this.supports, params, this.supportsAll);
}
/**
* Safely check certain param value
* @param value value to check
* @param defaultValue default value to return if check fails
* @param paramName name of the param to check value type against
* @return {boolean|number|*}
*/
getSafeParam(value, defaultValue, paramName) {
const t = this.constructor.getVarType;
function nest(suppNode, suppAllNode) {
if (t(suppNode) !== "object") return [suppNode, suppAllNode];
if (! suppNode.hasOwnProperty(paramName)) return [undefined, undefined];
return nest(suppNode[paramName], suppAllNode?.[paramName]);
}
const param = nest(this.supports, this.supportsAll), tParam = t(param[0]);
if (tParam === "object") {
console.warn("Parameters should not be stored at object level. No type inspection is done.");
return true; //no supported inspection
}
const tValue = t(value);
//supported type OR supports all types includes the type
if (tValue === tParam || (param[1] && param[1].map(t).includes(tValue))) {
return value;
}
if (tValue === "string") {
//try parsing NOTE: parsing from supportsAll is ignored!
if (tParam === "number") {
const parsed = Number.parseFloat(value);
if (!Number.isNaN(parsed)) return parsed;
} else if (tParam === "boolean") {
const val = value.toLowerCase();
if (val === "false") return false;
if (val === "true") return true;
}
}
//todo test
console.debug("Failed to load safe param -> new feature, debugging! ", value, defaultValue, paramName);
return defaultValue;
}
/**
* Uniform behaviour wrt type checking in shaders
* @param x
* @return {string}
*/
static getVarType(x) {
if (x === undefined) return "undefined";
if (x === null) return "null";
return Array.isArray(x) ? "array" : typeof x;
}
/**
* JavaScript initialization
* - read/store default properties here using this.context.[load/store]Property(...)
* - work with own HTML elements already attached to the DOM
* - set change listeners, input values!
*/
init() {
throw "WebGLModule.UIControls.IControl::init() must be implemented.";
}
/**
* Called when an image is rendered
* @param program WebglProgram instance
* @param dimension canvas dimension
* @param {number} dimension.width
* @param {number} dimension.height
* @param {(WebGLRenderingContext|WebGL2RenderingContext)} gl
*/
glDrawing(program, dimension, gl) {
//the control should send something to GPU
throw "WebGLModule.UIControls.IControl::glDrawing() must be implemented.";
}
/**
* Called when associated webgl program is switched to
* @param program WebglProgram instance
* @param gl WebGL Context
*/
glLoaded(program, gl) {
//the control should send something to GPU
throw "WebGLModule.UIControls.IControl::glLoaded() must be implemented.";
}
/**
* Get the UI HTML controls
* - these can be referenced in this.init(...)
* - should respect this.params.interactive attribute and return non-interactive output if interactive=false
* - don't forget to no to work with DOM elements in init(...) in this case
*
* todo: when overrided value before 'init' call on params, toHtml was already called, changes might not get propagated
* - either: delay toHtml to trigger insertion later (not nice)
* - do not allow changes before init call, these changes must happen at constructor
*/
toHtml(breakLine=true, controlCss="") {
throw "WebGLModule.UIControls.IControl::toHtml() must be implemented.";
}
/**
* Handles how the variable is being defined in GLSL
* - should use variable names derived from this.webGLVariableName
*/
define() {
throw "WebGLModule.UIControls.IControl::define() must be implemented.";
}
/**
* Sample the parameter using ratio as interpolation, must be one-liner expression so that GLSL code can write
* `vec3 mySampledValue = ${this.color.sample("0.2")};`
* TODO: if a control throws with the usage (type of the value does not fit), it is not properly reflected in the
* documentation!
*
* NOTE: you can define your own global-scope functions to keep one-lined sampling,
* see this.context.includeGlobalCode(...)
* @param {(string|undefined)} value openGL value/variable, used in a way that depends on the UI control currently active
* (do not pass arguments, i.e. 'undefined' just get that value, note that some inputs might require you do it..)
* @param {string} valueGlType GLSL type of the value
* @return {string} valid GLSL oneliner (wihtout ';') for sampling the value, or invalid code (e.g. error message) to signal error
*/
sample(value=undefined, valueGlType='void') {
throw "WebGLModule.UIControls.IControl::sample() must be implemented.";
}
/**
* Parameters supported by this UI component, must contain at least
* - 'interactive' - type bool, enables and disables the control interactivity
* (by changing the content available when rendering html)
* - 'title' - type string, the control title
*
* Additionally, for compatibility reasons, you should, if possible, define
* - 'default' - type any; the default value for the particular control
* @return {{}} name: default value mapping
*/
get supports() {
throw "WebGLModule.UIControls.IControl::supports must be implemented.";
}
/**
* Type definitions for supports. Can return empty object. In case of missing
* type definitions, the type is derived from the 'supports()' default value type.
*
* Each key must be an array of default values for the given key if applicable.
* This is an _extension_ to the supports() and can be used only for keys that have more
* than one default type applicable
* @return {{}}
*/
get supportsAll() {
throw "WebGLModule.UIControls.IControl::typeDefs must be implemented.";
}
/**
* GLSL type of this control: what type is returned from this.sample(...) ?
* @return {string}
*/
get type() {
throw "WebGLModule.UIControls.IControl::type must be implemented.";
}
/**
* Raw value sent to the GPU, note that not necessarily typeof raw() === type()
* some controls might send whole arrays of data (raw) and do smart sampling such that type is only a number
* @return {any}
*/
get raw() {
throw "WebGLModule.UIControls.IControl::raw must be implemented.";
}
/**
* Encoded value as used in the UI, e.g. a name of particular colormap, or array of string values of breaks...
* @return {any}
*/
get encoded() {
throw "WebGLModule.UIControls.IControl::encoded must be implemented.";
}
//////////////////////////////////////
//////// COMMON API //////////////////
//////////////////////////////////////
/**
* The control type component was registered with. Handled internally.
* @return {*}
*/
get uiControlType() {
return this.constructor._uiType;
}
/**
* Get current control parameters
* the control should set the value as this._params = this.getParams(incomingParams);
* @return {{}}
*/
get params() {
return this._params;
}
/**
* Automatically overridden to return the name of the control it was registered with
* @return {string}
*/
getName() {
return "IControl";
}
/**
* Load a value from cache to support its caching - should be used on all values
* that are available for the user to play around with and change using UI controls
*
* @param defaultValue value to return in case of no cached value
* @param paramName name of the parameter, must be equal to the name from 'supports' definition
* - default value can be empty string
* @return {*} cached or default value
*/
load(defaultValue, paramName="") {
if (paramName === "default") paramName = "";
const value = this.context.loadProperty(this.name + paramName, defaultValue);
//check param in case of input cache collision between shader types
return this.getSafeParam(value, defaultValue, paramName === "" ? "default" : paramName);
}
/**
* Store a value from cache to support its caching - should be used on all values
* that are available for the user to play around with and change using UI controls
*
* @param value to store
* @param paramName name of the parameter, must be equal to the name from 'supports' definition
* - default value can be empty string
*/
store(value, paramName="") {
if (paramName === "default") paramName = "";
return this.context.storeProperty(this.name + paramName, value);
}
/**
* On parameter change register self
* @param {string} event which event to fire on
* - events are with inputs the names of supported parameters (this.supports), separated by dot if nested
* - most controls support "default" event - change of default value
* - see specific control implementation to see what events are fired (Advanced Slider fires "breaks" and "mask" for instance)
* @param {function} clbck(rawValue, encodedValue, context) call once change occurs, context is the control instance
*/
on(event, clbck) {
this.__onchange[event] = clbck; //only one possible event -> rewrite?
}
/**
* Clear events of the event type
* @param {string} event type
*/
off(event) {
delete this.__onchange[event];
}
/**
* Clear ALL events
*/
clearEvents() {
this.__onchange = {}
}
/**
* Invoke changed value event
* -- should invoke every time a value changes !driven by USER!, and use unique or compatible
* event name (event 'value') so that shader knows what changed
* @param event event to call
* @param value decoded value of encodedValue
* @param encodedValue value that was received from the UI input
* @param context self reference to bind to the callback
*/
changed(event, value, encodedValue, context) {
if (typeof this.__onchange[event] === "function") {
this.__onchange[event](value, encodedValue, context);
}
}
__onchange = {}
};
/**
* Generic UI control implementations
* used if:
* {
* type: "CONTROL TYPE",
* ...
* }
*
* The subclass constructor should get the context reference, the name
* of the input and the parametrization.
*
* Further parameters passed are dependent on the control type, see
* @ WebGLModule.UIControls
*
* @class WebGLModule.UIControls.SimpleUIControl
*/
WebGLModule.UIControls.SimpleUIControl = class extends WebGLModule.UIControls.IControl {
//uses intristicComponent that holds all specifications needed to work with the component uniformly
constructor(context, name, webGLVariableName, params, intristicComponent, uniq="") {
super(context, name, webGLVariableName, uniq);
this.component = intristicComponent;
this._params = this.getParams(params);
}
init() {
this.encodedValue = this.load(this.params.default);
//this unfortunatelly makes cache erasing and rebuilding vis impossible, the shader part has to be fully re-instantiated
this.params.default = this.encodedValue;
this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params);
if (this.params.interactive) {
const _this = this;
let updater = function(e) {
_this.encodedValue = $(e.target).val();
_this.value = _this.component.normalize(_this.component.decode(_this.encodedValue), _this.params);
_this.changed("default", _this.value, _this.encodedValue, _this);
_this.store(_this.encodedValue);
_this.context.invalidate();
};
let node = $(`#${this.id}`);
node.val(this.encodedValue);
node.on('change', updater); //note, set change only now! val(..) would trigger it
}
}
glDrawing(program, dimension, gl) {
gl[this.component.glUniformFunName()](this.location_gluint, this.value);
}
glLoaded(program, gl) {
this.location_gluint = gl.getUniformLocation(program, this.webGLVariableName);
}
toHtml(breakLine=true, controlCss="") {
if (!this.params.interactive) return "";
const result = this.component.html(this.id, this.params, controlCss);
return breakLine ? `<div>${result}</div>` : result;
}
define() {
return `uniform ${this.component.glType} ${this.webGLVariableName};`;
}
sample(value=undefined, valueGlType='void') {
if (!value || valueGlType !== 'float') return this.webGLVariableName;
return this.component.sample(this.webGLVariableName, value);
}
get uiControlType() {
return this.component["uiType"];
}
get supports() {
return this.component.defaults();
}
get supportsAll() {
return {};
}
get raw() {
return this.value;
}
get encoded() {
return this.encodedValue;
}
get type() {
return this.component.glType;
}
};