/* * 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 vert(O) { return O.layout === "left" || O.layout === "right"; } function clip_timeout() { var O = this.options; if (!O.auto_clip || O.auto_clip < 0) return false; if (this.__cto) return; if (O.clip) this.__cto = window.setTimeout(this._reset_clip, O.auto_clip); } function peak_timeout() { var O = this.options; if (!O.auto_peak || O.auto_peak < 0) return false; if (this.__pto) window.clearTimeout(this.__pto); var value = +this.effective_value(); if (O.peak > O.base && value > O.base || O.peak < O.base && value < O.base) this.__pto = window.setTimeout(this._reset_peak, O.auto_peak); } function label_timeout() { var O = this.options; var peak_label = (0 | O.peak_label); var base = +O.base; var label = +O.label; var value = +this.effective_value(); if (peak_label <= 0) return false; if (this.__lto) window.clearTimeout(this.__lto); if (label > base && value > base || label < base && value < base) this.__lto = window.setTimeout(this._reset_label, peak_label); } function top_timeout() { var O = this.options; if (!O.auto_hold || O.auto_hold < 0) return false; if (this.__tto) window.clearTimeout(this.__tto); if (O.top > O.base) this.__tto = window.setTimeout( this._reset_top, O.auto_hold); else this.__tto = null; } function bottom_timeout() { var O = this.options; if (!O.auto_hold || O.auto_hold < 0) return false; if (this.__bto) window.clearTimeout(this.__bto); if (O.bottom < O.base) this.__bto = window.setTimeout(this._reset_bottom, O.auto_hold); else this.__bto = null; } TK.LevelMeter = TK.class({ /** * TK.LevelMeter is a fully functional meter bar displaying numerical values. * TK.LevelMeter is an enhanced {@link TK.MeterBase}'s containing a clip LED, * a peak pin with value label and hold markers. * In addition, LevelMeter has an optional falling animation, top and bottom peak * values and more. * * @class TK.LevelMeter * * @extends TK.MeterBase * * @param {Object} [options={ }] - An object containing initial options. * * @property {Boolean} [options.show_clip=false] - If set to true, show the clipping LED. * @property {Number} [options.clipping=0] - If clipping is enabled, this is the threshold for the * clipping effect. * @property {Integer|Boolean} [options.auto_clip=false] - This is the clipping timeout. If set to * false automatic clipping is disabled. If set to n the clipping effect * times out after n ms, if set to -1 it remains forever. * @property {Boolean} [options.clip=false] - If clipping is enabled, this option is set to * true when clipping happens. When automatic clipping is disabled, it can be set to * true to set the clipping state. * @property {Object} [options.clip_options={}] - Additional options for the {@link TK.State} clip LED. * @property {Boolean} [options.show_hold=false] - If set to true, show the hold value LED. * @property {Integer} [options.hold_size=1] - Size of the hold value LED in the number of segments. * @property {Number|boolean} [options.auto_hold=false] - If set to false the automatic * hold LED is disabled, if set to n the hold value is reset after n ms and * if set to -1 the hold value is not reset automatically. * @property {Number} [options.top=false] - The top hold value. If set to false it will * equal the meter level. * @property {Number} [options.bottom=false] - The bottom hold value. This only exists if a * base value is set and the value falls below the base. * @property {Boolean} [options.show_peak=false] - If set to true, show the peak label. * @property {Integer|Boolean} [options.peak_label=false] - If set to false the automatic peak * label is disabled, if set to n the peak label is reset after n ms and * if set to -1 it remains forever. * @property {Function} [options.format_peak=TK.FORMAT("%.2f")] - Formatting function for the peak label. * @property {Number} [options.falling=0] - If set to a positive number, activates the automatic falling * animation. The meter level will fall by this amount per frame. * @property {Number} [options.falling_fps=24] - This is the number of frames of the falling animation. * It is not an actual frame rate, but instead is used to determine the actual speed of the falling * animation together with the option falling. * @property {Number} [options.falling_init=2] - Initial falling delay in number of frames. This option * can be used to delay the start of the falling animation in order to avoid flickering if internal * and external falling are combined. */ _class: "LevelMeter", Extends: TK.MeterBase, _options: Object.assign(Object.create(TK.MeterBase.prototype._options), { falling: "number", falling_fps: "number", falling_init: "number", peak: "number", top: "number", bottom: "number", hold_size: "int", show_hold: "boolean", clipping: "number", auto_clip: "int|boolean", auto_peak: "int|boolean", peak_label: "int", auto_hold: "int|boolean", format_peak: "function", clip_options: "object", }), options: { clip: false, falling: 0, falling_fps: 24, falling_init: 2, peak: false, top: false, bottom: false, hold_size: 1, show_hold: false, clipping: 0, auto_clip: false, auto_peak: false, peak_label: false, auto_hold: false, format_peak: TK.FORMAT("%.2f"), clip_options: {} }, static_events: { set_label: label_timeout, set_bottom: bottom_timeout, set_top: top_timeout, set_peak: peak_timeout, set_clip: function(value) { if (value) { clip_timeout.call(this); } }, set_show_peak: peak_timeout, set_auto_clip: function(value) { if (this.__cto && 0|value <=0) window.clearTimeout(this.__cto); }, set_auto_peak: function(value) { if (this.__pto && 0|value <=0) window.clearTimeout(this.__pto); }, set_peak_label: function(value) { if (this.__lto && 0|value <=0) window.clearTimeout(this.__lto); }, set_auto_hold: function(value) { if (this.__tto && 0|value <=0) window.clearTimeout(this.__tto); if (this.__bto && 0|value <=0) window.clearTimeout(this.__bto); }, }, initialize: function (options) { /* track the age of the value option */ this.track_option("value"); TK.MeterBase.prototype.initialize.call(this, options); this._reset_label = this.reset_label.bind(this); this._reset_clip = this.reset_clip.bind(this); this._reset_peak = this.reset_peak.bind(this); this._reset_top = this.reset_top.bind(this); this._reset_bottom = this.reset_bottom.bind(this); /** * @member {HTMLDivElement} TK.LevelMeter#element - The main DIV container. * Has class toolkit-level-meter. */ TK.add_class(this.element, "toolkit-level-meter"); var O = this.options; if (O.peak === false) O.peak = O.value; if (O.top === false) O.top = O.value; if (O.bottom === false) O.bottom = O.value; if (O.falling < 0) O.falling = -O.falling; }, redraw: function () { var O = this.options; var I = this.invalid; var E = this.element; if (I.show_hold) { I.show_hold = false; TK.toggle_class(E, "toolkit-has-hold", O.show_hold); } if (I.top || I.bottom) { /* top and bottom require a meter redraw, so lets invalidate * value */ I.top = I.bottom = false; I.value = true; } if (I.base) I.value = true; TK.MeterBase.prototype.redraw.call(this); if (I.clip) { I.clip = false; TK.toggle_class(E, "toolkit-clipping", O.clip); } }, destroy: function () { TK.MeterBase.prototype.destroy.call(this); }, /** * Resets the peak label. * * @method TK.LevelMeter#reset_peak * * @emits TK.LevelMeter#resetpeak */ reset_peak: function () { if (this.__pto) clearTimeout(this.__pto); this.__pto = false; this.set("peak", this.effective_value()); /** * Is fired when the peak was reset. * * @event TK.LevelMeter#resetpeak */ this.fire_event("resetpeak"); }, /** * Resets the label. * * @method TK.LevelMeter#reset_label * * @emits TK.LevelMeter#resetlabel */ reset_label: function () { if (this.__lto) clearTimeout(this.__lto); this.__lto = false; this.set("label", this.effective_value()); /** * Is fired when the label was reset. * * @event TK.LevelMeter#resetlabel */ this.fire_event("resetlabel"); }, /** * Resets the clipping LED. * * @method TK.LevelMeter#reset_clip * * @emits TK.LevelMeter#resetclip */ reset_clip: function () { if (this.__cto) clearTimeout(this.__cto); this.__cto = false; this.set("clip", false); /** * Is fired when the clipping LED was reset. * * @event TK.LevelMeter#resetclip */ this.fire_event("resetclip"); }, /** * Resets the top hold. * * @method TK.LevelMeter#reset_top * * @emits TK.LevelMeter#resettop */ reset_top: function () { this.set("top", this.effective_value()); /** * Is fired when the top hold was reset. * * @event TK.LevelMeter#resettop */ this.fire_event("resettop"); }, /** * Resets the bottom hold. * * @method TK.LevelMeter#reset_bottom * * @emits TK.LevelMeter#resetbottom */ reset_bottom: function () { this.set("bottom", this.effective_value()); /** * Is fired when the bottom hold was reset. * * @event TK.LevelMeter#resetbottom */ this.fire_event("resetbottom"); }, /** * Resets all hold features. * * @method TK.LevelMeter#reset_all * * @emits TK.LevelMeter#resetpeak * @emits TK.LevelMeter#resetlabel * @emits TK.LevelMeter#resetclip * @emits TK.LevelMeter#resettop * @emits TK.LevelMeter#resetbottom */ reset_all: function () { this.reset_label(); this.reset_peak(); this.reset_clip(); this.reset_top(); this.reset_bottom(); }, effective_value: function() { var O = this.options; var falling = +O.falling; if (O.falling <= 0) return O.value; var value = +O.value, base = +O.base; var age = +this.value_time.value; if (!(age > 0)) age = Date.now(); else age = +(Date.now() - age); var frame_length = 1000.0 / +O.falling_fps; if (age > O.falling_init * frame_length) { if (value > base) { value -= falling * (age / frame_length); if (value < base) value = base; } else { value += falling * (age / frame_length); if (value > base) value = base; } } return value; }, /* * This is an _internal_ method, which calculates the non-filled regions * in the overlaying canvas as pixel positions. The canvas is only modified * using this information when it has _actually_ changed. This can save a lot * of performance in cases where the segment size is > 1 or on small devices where * the meter has a relatively small pixel size. */ calculate_meter: function(to, value, i) { var O = this.options; var falling = +O.falling; var base = +O.base; value = +value; // this is a bit unelegant... if (falling) { value = this.effective_value(); // continue animation if (value !== base) { this.invalid.value = true; // request another frame this.trigger_draw_next(); } } i = TK.MeterBase.prototype.calculate_meter.call(this, to, value, i); if (!O.show_hold) return i; // shorten things var hold = +O.top; var segment = O.segment|0; var hold_size = (O.hold_size|0) * segment; var base = +O.base; var pos; if (hold > base) { /* TODO: lets snap in set() */ pos = this.val2px(hold)|0; if (segment !== 1) pos = segment*(Math.round(pos/segment)|0); to[i++] = pos; to[i++] = pos+hold_size; } hold = +O.bottom; if (hold < base) { pos = this.val2px(hold)|0; if (segment !== 1) pos = segment*(Math.round(pos/segment)|0); to[i++] = pos; to[i++] = pos+hold_size; } return i; }, // GETTER & SETTER set: function (key, value) { if (key === "value") { var O = this.options; var base = O.base; // snap will enforce clipping value = this.snap(value); if (O.falling) { var v = this.effective_value(); if (v >= base && value >= base && value < v || v <= base && value <= base && value > v) { /* NOTE: we are doing a falling animation, but maybe its not running */ if (!this.invalid.value) { this.invalid.value = true; this.trigger_draw(); } return; } } if (O.auto_clip !== false && value > O.clipping && !this.has_base()) { this.set("clip", true); } if (O.show_label && O.peak_label !== false && (value > O.label && value > base || value < O.label && value < base)) { this.set("label", value); } if (O.auto_peak !== false && (value > O.peak && value > base || value < O.peak && value < base)) { this.set("peak", value); } if (O.auto_hold !== false && O.show_hold && value > O.top) { this.set("top", value); } if (O.auto_hold !== false && O.show_hold && value < O.bottom && this.has_base()) { this.set("bottom", value); } } else if (key === "top" || key === "bottom") { value = this.snap(value); } return TK.MeterBase.prototype.set.call(this, key, value); } }); /** * @member {TK.State} TK.LevelMeter#clip - The {@link TK.State} instance for the clipping LED. * @member {HTMLDivElement} TK.LevelMeter#clip.element - The DIV element of the clipping LED. * Has class toolkit-clip. */ TK.ChildWidget(TK.LevelMeter, "clip", { create: TK.State, show: false, map_options: { clip: "state", }, default_options: { "class": "toolkit-clip" }, toggle_class: true, }); /** * @member {HTMLDivElement} TK.LevelMeter#_peak - The DIV element for the peak marker. * Has class toolkit-peak. */ TK.ChildElement(TK.LevelMeter, "peak", { show: false, create: function() { var peak = TK.element("div","toolkit-peak"); peak.appendChild(TK.element("div","toolkit-peak-label")); return peak; }, append: function() { this._bar.appendChild(this._peak); }, toggle_class: true, draw_options: [ "peak" ], draw: function (O) { if (!this._peak) return; var n = this._peak.firstChild; TK.set_text(n, O.format_peak(O.peak)); if (O.peak > O.min && O.peak < O.max && O.show_peak) { this._peak.style.display = "block"; var pos = 0; if (vert(O)) { pos = O.basis - this.val2px(this.snap(O.peak)); pos = Math.min(O.basis, pos); this._peak.style.top = pos + "px"; } else { pos = this.val2px(this.snap(O.peak)); pos = Math.min(O.basis, pos) this._peak.style.left = pos + "px"; } } else { this._peak.style.display = "none"; } /** * Is fired when the peak was drawn. * * @event TK.LevelMeter#drawpeak */ this.fire_event("drawpeak"); }, }); })(this, this.TK);