/* * This file is part of Toolkit. * * Toolkit is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * Toolkit is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU General * Public License along with this program; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA */ "use strict"; (function(w, TK){ function LinearSnapModule(stdlib, foreign) { var min = +foreign.min; var max = +foreign.max; var step = +foreign.step; var base = +foreign.base; var floor = stdlib.Math.floor; var ceil = stdlib.Math.ceil; function low_snap(v, direction) { v = +v; direction = +direction; var n = 0.0; var t = 0.0; if (!(v > min)) { v = min; direction = 1.0; } else if (!(v < max)) { v = max; direction = +1.0; } t = (v - base)/step; if (direction > 0.0) n = ceil(t); else if (direction < 0.0) n = floor(t); else { if (t - floor(t) < 0.5) { n = floor(t); } else { n = ceil(t); } } return base + step * n; } /** * Returns the nearest value on the grid which is bigger than value. * * @method TK.Ranged#snap_up * * @param {number} value - The value to snap. * * @returns {number} The snapped value. */ function snap_up(v) { v = +v; return +low_snap(v, 1.0); } /** * Returns the nearest value on the grid which is smaller than value. * * @method TK.Ranged#snap_down * * @param {number} value - The value to snap. * * @returns {number} The snapped value. */ function snap_down(v) { v = +v; return +low_snap(v, -1.0); } /** * Returns the nearest value on the grid. Its rounding behavior is similar to that * of Math.round. * * @method TK.Ranged#snap * * @param {number} value - The value to snap. * * @returns {number} The snapped value. */ function snap(v) { v = +v; return +low_snap(v, 0.0); } return { snap_up : snap_up, snap_down : snap_down, snap : snap }; } function ArraySnapModule(stdlib, foreign, heap) { var values = new stdlib.Float64Array(heap); var len = (heap.byteLength>>3)|0; var min = +(foreign.min !== void 0 ? foreign.min : values[0]); var max = +(foreign.max !== void 0 ? foreign.max : values[len-1]); function low_snap(v, direction) { v = +v; direction = +direction; var a = 0; var mid = 0; var b = 0; var t = 0.0; b = len-1; if (!(v > min)) v = min; if (!(v < max)) v = max; if (!(v < +values[b << 3 >> 3])) return +values[b << 3 >> 3]; if (!(v > +values[0])) return +values[0]; do { mid = (a + b) >>> 1; t = +values[mid << 3 >> 3]; if (v > t) a = mid; else if (v < t) b = mid; else return t; } while (((b - a)|0) > 1); if (direction > 0.0) return +values[b << 3 >> 3]; else if (direction < 0.0) return +values[a << 3 >> 3]; if (values[b << 3 >> 3] - v <= v - values[a << 3 >> 3]) return +values[b << 3 >> 3]; return +values[a << 3 >> 3]; } function snap_up(v) { v = +v; return +low_snap(v, 1.0); } function snap_down(v) { v = +v; return +low_snap(v, -1.0); } function snap(v) { v = +v; return +low_snap(v, 0.0); } return { snap_up : snap_up, snap_down : snap_down, snap : snap }; } function NullSnapModule(stdlib, foreign, heap) { var min = +foreign.min; var max = +foreign.max; function snap(v) { v = +v; if (!(v < max)) v = max; if (!(v > min)) v = min; return v; } return { snap: snap, snap_up: snap, snap_down: snap, }; } function num_sort(a) { a = a.slice(0); a.sort(function(a, b) { return a - b; }); return a; } function update_snap() { var O = this.options; // Notify that the ranged options have been modified if (Array.isArray(O.snap)) { Object.assign(this, ArraySnapModule(window, O, new Float64Array(num_sort(O.snap)).buffer)); } else if (typeof O.snap === "number" && O.snap > 0.0) { Object.assign(this, LinearSnapModule(window, { min : Math.min(O.min, O.max), max : Math.max(O.min, O.max), step : O.snap, base: O.base||0 })); } else if (O.min < Infinity && O.max > -Infinity) { Object.assign(this, NullSnapModule(window, { min : Math.min(O.min, O.max), max : Math.max(O.min, O.max) })); } else { Object.assign(this, { snap: function(v) { return +v; }, snap_up: function(v) { return +v; }, snap_down: function(v) { return +v; }, }); } } function TRAFO_PIECEWISE(stdlib, foreign, heap) { var reverse = foreign.reverse|0; var l = heap.byteLength >> 4; var X = new Float64Array(heap, 0, l); var Y = new Float64Array(heap, l*8, l); var basis = +foreign.basis; function val2based(coef, size) { var a = 0, b = (l-1)|0, mid = 0, t = 0.0; coef = +coef; size = +size; if (!(coef > +Y[0])) return +X[0] * size; if (!(coef < +Y[b << 3 >> 3])) return +X[b << 3 >> 3] * size; do { mid = (a + b) >>> 1; t = +Y[mid << 3 >> 3]; if (coef > t) a = mid; else if (coef < t) b = mid; else return +X[mid << 3 >> 3] * size; } while (((b - a)|0) > 1); /* value lies between a and b */ t = (+X[b << 3 >> 3] - +X[a << 3 >> 3]) / (+Y[b << 3 >> 3] - +Y[a << 3 >> 3]); t = +X[a << 3 >> 3] + (coef - +Y[a << 3 >> 3]) * t; t *= size; if (reverse) t = size - t; return t; } function based2val(coef, size) { var a = 0, b = (l-1)|0, mid = 0, t = 0.0; coef = +coef; size = +size; if (reverse) coef = size - coef; coef /= size; if (!(coef > 0)) return Y[0]; if (!(coef < 1)) return Y[b << 3 >> 3]; do { mid = (a + b) >>> 1; t = +X[mid << 3 >> 3]; if (coef > t) a = mid; else if (coef < t) b = mid; else return +Y[mid << 3 >> 3]; } while (((b - a)|0) > 1); /* value lies between a and b */ t = (+Y[b << 3 >> 3] - +Y[a << 3 >> 3]) / (+X[b << 3 >> 3] - +X[a << 3 >> 3]); return +Y[a << 3 >> 3] + (coef - +X[a << 3 >> 3]) * t; } function val2px(n) { return val2based(n, basis || 1); } function px2val(n) { return based2val(n, basis || 1); } function val2coef(n) { return val2based(n, 1); } function coef2val(n) { return based2val(n, 1); } return { val2based:val2based, based2val:based2val, val2px:val2px, px2val:px2val, val2coef:val2coef, coef2val:coef2val, }; }; function TRAFO_FUNCTION(stdlib, foreign) { var reverse = foreign.reverse|0; var min = +foreign.min; var max = +foreign.max; var scale = foreign.scale; var basis = +foreign.basis; function val2based(value, size) { value = +value; size = +size; value = scale(value, foreign, false) * size; if (reverse) value = size - value; return value; } function based2val(coef, size) { coef = +coef; size = +size; if (reverse) coef = size - coef; coef = scale(coef/size, foreign, true); return coef; } function val2px(n) { return val2based(n, basis || 1); } function px2val(n) { return based2val(n, basis || 1); } function val2coef(n) { return val2based(n, 1); } function coef2val(n) { return based2val(n, 1); } return { val2based:val2based, based2val:based2val, val2px:val2px, px2val:px2val, val2coef:val2coef, coef2val:coef2val, }; } function TRAFO_LINEAR(stdlib, foreign) { var reverse = foreign.reverse|0; var min = +foreign.min; var max = +foreign.max; var basis = +foreign.basis; function val2based(value, size) { value = +value; size = +size; value = ((value - min) / (max - min)) * size; if (reverse) value = size - value; return value; } function based2val(coef, size) { coef = +coef; size = +size; if (reverse) coef = size - coef; coef = (coef / size) * (max - min) + min; return coef; } // just a wrapper for having understandable code and backward // compatibility function val2px(n) { n = +n; if (basis == 0.0) basis = 1.0; return +val2based(n, basis); } // just a wrapper for having understandable code and backward // compatibility function px2val(n) { n = +n; if (basis == 0.0) basis = 1.0; return +based2val(n, basis); } // calculates a coefficient for the value function val2coef(n) { n = +n; return +val2based(n, 1.0); } // calculates a value from a coefficient function coef2val(n) { n = +n; return +based2val(n, 1.0); } return { /** * Transforms a value from the coordinate system to the interval 0...basis. * * @method TK.Ranged#val2based * * @param {number} value * @param {number} [basis=1] * * @returns {number} */ val2based:val2based, /** * Transforms a value from the interval 0...basis to the coordinate system. * * @method TK.Ranged#based2val * * @param {number} value * @param {number} [basis=1] * * @returns {number} */ based2val:based2val, /** * This is an alias for {@link TK.Ranged#val2px}. * * @method TK.Ranged#val2px * * @param {number} value * * @returns {number} */ val2px:val2px, /** * This is an alias for {@link TK.Ranged#px2val}. * * @method TK.Ranged#px2val * * @param {number} value * * @returns {number} */ px2val:px2val, /** * Calls {@link based2val} with basis = 1. * * @method TK.Ranged#val2coef * * @param {number} value * * @returns {number} */ val2coef:val2coef, /** * Calls {@link based2val} with basis = 1. * * @method TK.Ranged#coef2val * * @param {number} value * * @returns {number} */ coef2val:coef2val, }; } function TRAFO_LOG(stdlib, foreign) { var db2scale = stdlib.TK.AudioMath.db2scale; var scale2db = stdlib.TK.AudioMath.scale2db; var reverse = foreign.reverse|0; var min = +foreign.min; var max = +foreign.max; var log_factor = +foreign.log_factor; var trafo_reverse = foreign.trafo_reverse|0; var basis = +foreign.basis; function val2based(value, size) { value = +value; size = +size; value = +db2scale(value, min, max, size, trafo_reverse, log_factor); if (reverse) value = size - value; return value; } function based2val(coef, size) { coef = +coef; size = +size; if (reverse) coef = size - coef; coef = +scale2db(coef, min, max, size, trafo_reverse, log_factor); return coef; } function val2px(n) { return val2based(n, basis || 1); } function px2val(n) { return based2val(n, basis || 1); } function val2coef(n) { return val2based(n, 1); } function coef2val(n) { return based2val(n, 1); } return { val2based:val2based, based2val:based2val, val2px:val2px, px2val:px2val, val2coef:val2coef, coef2val:coef2val, }; } function TRAFO_FREQ(stdlib, foreign) { var freq2scale = stdlib.TK.AudioMath.freq2scale; var scale2freq = stdlib.TK.AudioMath.scale2freq; var reverse = foreign.reverse|0; var min = +foreign.min; var max = +foreign.max; var trafo_reverse = foreign.trafo_reverse|0; var basis = +foreign.basis; function val2based(value, size) { value = +value; size = +size; value = +freq2scale(value, min, max, size, trafo_reverse); if (reverse) value = size - value; return value; } function based2val(coef, size) { coef = +coef; size = +size; if (reverse) coef = size - coef; coef = +scale2freq(coef, min, max, size, trafo_reverse); return coef; } function val2px(n) { return val2based(n, basis || 1); } function px2val(n) { return based2val(n, basis || 1); } function val2coef(n) { return val2based(n, 1); } function coef2val(n) { return based2val(n, 1); } return { val2based:val2based, based2val:based2val, val2px:val2px, px2val:px2val, val2coef:val2coef, coef2val:coef2val, }; } function update_transformation() { var O = this.options; var scale = O.scale; var module; if (typeof scale === "function") { module = TRAFO_FUNCTION(w, O); } else if (Array.isArray(scale)) { var i = 0; if (scale.length % 2) { TK.error("Malformed piecewise-linear scale."); } for (i = 0; i < scale.length/2 - 1; i++) { if (!(scale[i] >= 0 && scale[i] <= 1)) TK.error("piecewise-linear x value not in [0,1]."); if (!(scale[i] < scale[i+1])) TK.error("piecewise-linear array not sorted."); } for (i = scale.length/2; i < scale.length - 1; i++) { if (!(scale[i] < scale[i+1])) TK.error("piecewise-linear array not sorted."); } module = TRAFO_PIECEWISE(w, O, new Float64Array(scale).buffer); } else switch (scale) { case "linear": module = TRAFO_LINEAR(w, O); break; case "decibel": O.trafo_reverse = 1; module = TRAFO_LOG(w, O); break; case "log2": O.trafo_reverse = 0; module = TRAFO_LOG(w, O); break; case "frequency": O.trafo_reverse = 0; module = TRAFO_FREQ(w, O); break; case "frequency-reverse": O.trafo_reverse = 1; module = TRAFO_FREQ(w, O); break; default: TK.warn("Unsupported scale", scale); } Object.assign(this, module); } function set_cb(key, value) { switch (key) { case "min": case "max": case "snap": update_snap.call(this); /* fall through */ case "log_factor": case "scale": case "reverse": case "basis": update_transformation.call(this); this.fire_event("rangedchanged"); break; } } /** * @callback TK.Ranged~scale_cb * * @param {number} value - The value to be transformed. * @param {Object} [options={ }] - An object containing initial options. - The options of the corresponding {@link TK.Ranged} object. * @param {boolean} [inverse=false] - Determines if the value is to be transformed from or * to the coordinate system. * * @returns {number} The transformed value. */ TK.Ranged = TK.class({ /** * TK.Ranged combines functionality for two distinct purposes. * Firstly, TK.Ranged can be used to snap values to a virtual grid. * This grid is defined by the options snap, * step, min, max and base. * The second feature of TK.anged is that it allows transforming values between coordinate systems. * This can be used to transform values from and to linear scales in which they are displayed on the * screen. It is used inside of Toolkit to translate values (e.g. in Hz or dB) to pixel positions or * percentages, for instance in widgets such as {@link TK.Scale}, {@link TK.MeterBase} or * {@link TK.Graph}. * * TK.Ranged features several types of coordinate systems which are often used in audio applications. * They can be configured using the options.scale option, possible values are: * * If options.scale is a function, it is used as the coordinate transformation. * Its signature is {@link TK.Ranged~scale_cb}. This allows the definition of custom * coordinate transformations, which go beyond the standard types. * * @param {Object} [options={ }] - An object containing initial options. * * @property {String|Array|Function} [options.scale="linear"] - * The type of the scale. Either one of linear, decibel, log2, * frequency or frequency-reverse; or an array containing a * piece-wise linear scale; * or a callback function of type {@link TK.Ranged~scale_cb}. * @property {Boolean} [options.reverse=false] - Reverse the scale of the range. * @property {Number} [options.basis=1] - The size of the linear scale. Set to pixel width or height * if used for drawing purposes or to 100 for percentages. * @property {Number} [options.min=0] - Minimum value of the range. * @property {Number} [options.max=1] - Maximum value of the range. * @property {Number} [options.log_factor=1] - Used to overexpand logarithmic curves. 1 keeps the * natural curve while values above 1 will overbend. * @property {Number|Array.} [options.snap=0] - * Defines a virtual grid. * If options.snap is a positive number, it is interpreted as the distance of * grid points. * Then, inside of the interval options.min ... options.max the grid * points are options.base + n * options.snap where n is any * integer. Any values outside of that interval are snapped to the biggest or smallest grid * point, respectively. * In order to define grids with non-uniform spacing, set options.snap to an Array * of grid points. * @property {Number} [options.base=0] - Base point. Used e.g. to mark 0dB on a fader from -96dB to 12dB. * @property {Number} [options.step=0] - Step size. Used for instance by {@link TK.ScrollValue} * as the step size. * @property {Number} [options.shift_up=4] - Multiplier for increased stepping speed, e.g. used by * {@link TK.ScrollValue} when simultaneously pressing 'shift'. * @property {Number} [options.shift_down=0.25] - Multiplier for descresed stepping speed, e.g. used by * {@link TK.ScrollValue} when simultaneously pressing 'shift' and 'ctrl'. * * @mixin TK.Ranged */ _class: "Ranged", options: { scale: "linear", reverse: false, basis: 1, min: 0, max: 1, base: 0, step: 0, shift_up: 4, shift_down: 0.25, snap: 0, round: true, /* default for TK.Range, no dedicated option */ log_factor: 1, trafo_reverse: false, /* used internally, no documentation */ }, _options: { scale: "string|array|function", reverse: "boolean", basis: "number", min: "number", max: "number", base: "number", step: "number", shift_up: "number", shift_down: "number", snap: "mixed", round: "boolean", log_factor: "number", trafo_reverse: "boolean", }, static_events: { set: set_cb, initialized: function() { var O = this.options; if (!(O.min <= O.max)) TK.warn("Ranged needs min <= max. min: ", O.min, ", max:", O.max, ", options:", O); update_snap.call(this); update_transformation.call(this); }, }, }); })(this, this.TK);