mirror of
https://github.com/openseadragon/openseadragon.git
synced 2025-03-28 11:03:53 +03:00
1306 lines
46 KiB
JavaScript
1306 lines
46 KiB
JavaScript
(function($) {
|
|
|
|
/**
|
|
* Shader sharing point
|
|
* @class OpenSeadragon.WebGLModule.ShaderMediator
|
|
*/
|
|
$.WebGLModule.ShaderMediator = class {
|
|
|
|
/**
|
|
* Register shader
|
|
* @param {function} LayerRendererClass class extends OpenSeadragon.WebGLModule.ShaderLayer
|
|
*/
|
|
static registerLayer(LayerRendererClass) {
|
|
//todo why not hasOwnProperty check allowed by syntax checker
|
|
// if (this._layers.hasOwnProperty(LayerRendererClass.type())) {
|
|
// console.warn("Registering an already existing layer renderer:", LayerRendererClass.type());
|
|
// }
|
|
// if (!$.WebGLModule.ShaderLayer.isPrototypeOf(LayerRendererClass)) {
|
|
// throw `${LayerRendererClass} does not inherit from ShaderLayer!`;
|
|
// }
|
|
this._layers[LayerRendererClass.type()] = LayerRendererClass;
|
|
}
|
|
|
|
/**
|
|
* Get the shader class by type id
|
|
* @param {string} id
|
|
* @return {function} class extends OpenSeadragon.WebGLModule.ShaderLayer
|
|
*/
|
|
static getClass(id) {
|
|
return this._layers[id];
|
|
}
|
|
|
|
/**
|
|
* Get all available shaders
|
|
* @return {function[]} classes that extend OpenSeadragon.WebGLModule.ShaderLayer
|
|
*/
|
|
static availableShaders() {
|
|
return Object.values(this._layers);
|
|
}
|
|
};
|
|
//todo why cannot be inside object :/
|
|
$.WebGLModule.ShaderMediator._layers = {};
|
|
|
|
|
|
/**
|
|
* Abstract interface to any Shader.
|
|
* @abstract
|
|
*/
|
|
$.WebGLModule.ShaderLayer = 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 "ShaderLayer::type() Type must be specified!";
|
|
}
|
|
|
|
/**
|
|
* Override **static** name definition
|
|
* @returns {string} name of the shader (user-friendly)
|
|
*/
|
|
static name() {
|
|
throw "ShaderLayer::name() Name must be specified!";
|
|
}
|
|
|
|
/**
|
|
* Provide description
|
|
* @returns {string} optional description
|
|
*/
|
|
static description() {
|
|
return "ShaderLayer::description() WebGL shader must provide description.";
|
|
}
|
|
|
|
/**
|
|
* 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 "ShaderLayer::sources() Shader must specify channel acceptance predicates for each source it uses!";
|
|
}
|
|
|
|
/**
|
|
* Global supported options
|
|
* @param {string} id unique ID among all webgl instances and shaders
|
|
* @param {OpenSeadragon.WebGLModule.ShaderLayerParams} options
|
|
* options.channel: "r", "g" or "b" channel to sample, 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 {object} privateOptions options that should not be touched, necessary for linking the layer to the core
|
|
*/
|
|
constructor(id, options, privateOptions) {
|
|
this.uid = id;
|
|
this._setContextShaderLayer(privateOptions.layer);
|
|
this.webglContext = privateOptions.webgl;
|
|
this.invalidate = privateOptions.invalidate;
|
|
//use with care...
|
|
this._rebuild = privateOptions.rebuild;
|
|
|
|
this._buildControls(options);
|
|
this.resetChannel(options);
|
|
this.resetMode(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 controls = this.constructor.defaultControls,
|
|
html = [];
|
|
for (let control in controls) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
const controlObject = this[control];
|
|
if (controlObject) {
|
|
let code = controlObject.define();
|
|
if (code) {
|
|
code = code.trim();
|
|
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 "ShaderLayer::getFragmentShaderExecution must be implemented!";
|
|
}
|
|
|
|
/**
|
|
* Called when an image is rendered
|
|
* @param {WebGLProgram} program WebglProgram instance
|
|
* @param {WebGLRenderingContextBase} gl
|
|
*/
|
|
glDrawing(program, gl) {
|
|
let controls = this.constructor.defaultControls;
|
|
for (let control in controls) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
const controlObject = this[control];
|
|
if (controlObject) {
|
|
controlObject.glDrawing(program, gl);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when associated webgl program is switched to
|
|
* @param {WebGLProgram} program WebglProgram instance
|
|
* @param {WebGLRenderingContextBase} gl WebGL Context
|
|
*/
|
|
glLoaded(program, gl) {
|
|
let controls = this.constructor.defaultControls;
|
|
for (let control in controls) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
const controlObject = this[control];
|
|
if (controlObject) {
|
|
controlObject.glLoaded(program, gl);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function is called once at
|
|
* the beginning of the layer use
|
|
* (might be multiple times), after htmlControls()
|
|
*/
|
|
init() {
|
|
let controls = this.constructor.defaultControls;
|
|
for (let control in controls) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
const controlObject = this[control];
|
|
if (controlObject) {
|
|
controlObject.init();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the shader UI controls
|
|
* @return {string} HTML controls for the particular shader
|
|
*/
|
|
htmlControls() {
|
|
let controls = this.constructor.defaultControls,
|
|
html = [];
|
|
for (let control in controls) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
const controlObject = this[control];
|
|
if (controlObject) {
|
|
html.push(controlObject.toHtml(true));
|
|
}
|
|
}
|
|
return html.join("");
|
|
}
|
|
|
|
/**
|
|
* 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[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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.__visualisationLayer.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.texture.sample(refs[otherDataIndex], textureCoords)}.${chan}`;
|
|
// if (raw) return sampled;
|
|
// return this.filter(sampled);
|
|
return sampled;
|
|
}
|
|
|
|
/**
|
|
* For error detection, how many textures are available
|
|
* @return {number} number of textures available
|
|
*/
|
|
dataSourcesCount() {
|
|
return this.__visualisationLayer.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.__visualisationLayer) {
|
|
return defaultValue;
|
|
}
|
|
|
|
const value = this.__visualisationLayer.cache[selfType][name];
|
|
return value === undefined ? defaultValue : value;
|
|
}
|
|
|
|
/**
|
|
* Store value, useful for controls value caching
|
|
* @param {string} name value name
|
|
* @param {*} value value
|
|
*/
|
|
storeProperty(name, value) {
|
|
this.__visualisationLayer.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.__visualisationLayer.dataReferences.length;
|
|
}
|
|
|
|
/**
|
|
* 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[name] || predefined) {
|
|
let channel = predefined ? (predefined.required ? predefined.required : predefined.default) : undefined;
|
|
if (!channel) {
|
|
channel = this.loadProperty(name, options[name]);
|
|
}
|
|
|
|
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["use_mode"]) {
|
|
this._mode = predefined && predefined.required;
|
|
if (!this._mode) {
|
|
this._mode = this.loadProperty("use_mode", options.use_mode);
|
|
}
|
|
|
|
if (this._mode !== options.use_mode) {
|
|
this.storeProperty("use_mode", this._mode);
|
|
}
|
|
} else {
|
|
this._mode = predefined ? (predefined.default || "show") : "show";
|
|
}
|
|
|
|
this.__mode = this.constructor.modes[this._mode] || "show";
|
|
}
|
|
|
|
////////////////////////////////////
|
|
////////// PRIVATE /////////////////
|
|
////////////////////////////////////
|
|
|
|
|
|
_buildControls(options) {
|
|
let controls = this.constructor.defaultControls;
|
|
|
|
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) {
|
|
let buildContext = controls[control];
|
|
|
|
if (buildContext) {
|
|
if (control.startsWith("use_")) {
|
|
continue;
|
|
}
|
|
|
|
this[control] = $.WebGLModule.UIControls.build(this, control, options[control],
|
|
buildContext.default, buildContext.accepts, buildContext.required);
|
|
}
|
|
}
|
|
}
|
|
|
|
_setContextShaderLayer(visualisationLayer) {
|
|
this.__visualisationLayer = visualisationLayer;
|
|
if (!this.__visualisationLayer.cache) {
|
|
this.__visualisationLayer.cache = {};
|
|
}
|
|
if (!this.__visualisationLayer.cache[this.constructor.type()]) {
|
|
this.__visualisationLayer.cache[this.constructor.type()] = {};
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 to use_[filter][X] to distinguish between textures
|
|
*
|
|
* @member {object}
|
|
*/
|
|
$.WebGLModule.ShaderLayer.defaultControls = {};
|
|
|
|
|
|
/**
|
|
* todo make blending more 'nice'
|
|
* Available use_mode modes
|
|
* @type {{show: string, mask: string}}
|
|
*/
|
|
$.WebGLModule.ShaderLayer.modes = {
|
|
show: "show",
|
|
mask: "blend"
|
|
};
|
|
$.WebGLModule.ShaderLayer.modes["mask_clip"] = "blend_clip"; //todo parser error not camel case
|
|
$.WebGLModule.ShaderLayer.__globalIncludes = {};
|
|
$.WebGLModule.ShaderLayer.__chanPattern = new RegExp('[rgba]{1,4}');
|
|
|
|
/**
|
|
* 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 OpenSeadragon.WebGLModule.UIControls.register()
|
|
* @class OpenSeadragon.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 {OpenSeadragon.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 {OpenSeadragon.WebGLModule.ShaderLayer} 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
|
|
* @return {OpenSeadragon.WebGLModule.UIControls.IControl}
|
|
*/
|
|
static build(context, name, params, defaultParams = {}, accepts = () => true, requiredParams = {}) {
|
|
//if not an object, but a value: make it the default one
|
|
if (!(typeof params === 'object')) {
|
|
params = {default: params};
|
|
}
|
|
let originalType = defaultParams.type;
|
|
|
|
defaultParams = $.extend(true, {}, defaultParams, params, requiredParams);
|
|
|
|
if (!this._items[defaultParams.type]) {
|
|
if (!this._impls[defaultParams.type]) {
|
|
return this._buildFallback(defaultParams.type, originalType, context,
|
|
name, params, defaultParams, accepts, requiredParams);
|
|
}
|
|
|
|
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);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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[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[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 {OpenSeadragon.WebGLModule.UIControls.IControl} cls to register, implementation class of the controls
|
|
*/
|
|
static registerClass(type, cls) {
|
|
//todo not really possible with syntax checker :/
|
|
// if ($.WebGLModule.UIControls.IControl.isPrototypeOf(cls)) {
|
|
cls.prototype.getName = () => type;
|
|
|
|
if (this._items[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 /////////
|
|
/////////////////////////
|
|
|
|
|
|
static _buildFallback(newType, originalType, context, name, params, defaultParams, requiredType, requiredParams) {
|
|
//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);
|
|
}
|
|
}
|
|
};
|
|
|
|
//implementation of UI control classes
|
|
//more complex functionality
|
|
$.WebGLModule.UIControls._impls = {
|
|
//colormap: $.WebGLModule.UIControls.ColorMap
|
|
};
|
|
//implementation of UI control objects
|
|
//simple functionality
|
|
$.WebGLModule.UIControls._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"
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @interface
|
|
*/
|
|
$.WebGLModule.UIControls.IControl = class {
|
|
|
|
/**
|
|
* Sets common properties needed to create the controls:
|
|
* this.context @extends WebGLModule.ShaderLayer - 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.ShaderLayer} 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 = {};
|
|
this.__onchange = {};
|
|
}
|
|
|
|
/**
|
|
* 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 = Object.assign({}, mask);
|
|
Object.keys(from).forEach(key => {
|
|
const tVal = to[key],
|
|
fVal = from[key],
|
|
tType = t(tVal),
|
|
fType = t(fVal);
|
|
|
|
const typeList = possibleTypes ? possibleTypes[key] : undefined,
|
|
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[paramName]) {
|
|
return [undefined, undefined];
|
|
}
|
|
return nest(suppNode[paramName], suppAllNode ? suppAllNode[paramName] : undefined);
|
|
}
|
|
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.";
|
|
}
|
|
|
|
/**
|
|
* TODO: improve overall setter API
|
|
* Allows to set the control value programatically.
|
|
* Does not trigger canvas re-rednreing, must be done manually (e.g. control.context.invalidate())
|
|
* @param encodedValue any value the given control can support, encoded
|
|
* (e.g. as the control acts on the GUI - for input number of
|
|
* values between 5 and 42, the value can be '6' or 6 or 6.15
|
|
*/
|
|
set(encodedValue) {
|
|
throw "WebGLModule.UIControls.IControl::set() must be implemented.";
|
|
}
|
|
|
|
/**
|
|
* Called when an image is rendered
|
|
* @param program WebglProgram instance
|
|
* @param {WebGLRenderingContextBase} gl
|
|
*/
|
|
glDrawing(program, 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
|
|
*/
|
|
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")};`
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
* 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);
|
|
|
|
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;
|
|
}
|
|
|
|
init() {
|
|
this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params);
|
|
|
|
if (this.params.interactive) {
|
|
const _this = this;
|
|
let node = document.getElementById(this.id);
|
|
if (node) {
|
|
let updater = function(e) {
|
|
_this.set(e.target.value);
|
|
_this.context.invalidate();
|
|
};
|
|
node.value = this.encodedValue;
|
|
node.addEventListener('change', updater);
|
|
}
|
|
}
|
|
}
|
|
|
|
set(encodedValue) {
|
|
this.encodedValue = encodedValue;
|
|
this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params);
|
|
this.changed("default", this.value, this.encodedValue, this);
|
|
this.store(this.encodedValue);
|
|
}
|
|
|
|
glDrawing(program, gl) {
|
|
gl[this.component.glUniformFunName()](this.glLocation, this.value);
|
|
}
|
|
|
|
glLoaded(program, gl) {
|
|
this.glLocation = 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;
|
|
}
|
|
};
|
|
})(OpenSeadragon);
|