/* * 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 fill_interval(ctx, w, h, a, is_vertical) { var i; if (is_vertical) { for (i = 0; i < a.length; i+= 2) { ctx.fillRect(0, h - a[i+1], w, a[i+1]-a[i]); } } else { for (i = 0; i < a.length; i+= 2) { ctx.fillRect(a[i], 0, a[i+1]-a[i], h); } } } function clear_interval(ctx, w, h, a, is_vertical) { var i; if (is_vertical) { for (i = 0; i < a.length; i+= 2) { ctx.clearRect(0, h - a[i+1], w, a[i+1]-a[i]); } } else { for (i = 0; i < a.length; i+= 2) { ctx.clearRect(a[i], 0, a[i+1]-a[i], h); } } } function draw_full(ctx, w, h, a, is_vertical) { ctx.fillRect(0, 0, w, h); clear_interval(ctx, w, h, a, is_vertical); } function make_interval(a) { var i, tmp, again, j; do { again = false; for (i = 0; i < a.length-2; i+=2) { if (a[i] > a[i+2]) { tmp = a[i]; a[i] = a[i+2]; a[i+2] = tmp; tmp = a[i+1]; a[i+1] = a[i+3]; a[i+3] = tmp; again = true; } } } while (again); for (i = 0; i < a.length-2; i+= 2) { if (a[i+1] > a[i+2]) { if (a[i+3] > a[i+1]) { a[i+1] = a[i+3]; } for (j = i+3; j < a.length; j++) a[j-1] = a[j]; a.length = j-2; i -=2; continue; } } } function cmp_intervals(a, b) { var ret = 0; var i; for (i = 0; i < a.length; i+=2) { if (a[i] === b[i]) { if (a[i+1] === b[i+1]) continue; ret |= (a[i+1] < b[i+1]) ? 1 : 2; } else if (a[i+1] === b[i+1]) { ret |= (a[i] > b[i]) ? 1 : 2; } else return 4; } return ret; } function subtract_intervals(a, b) { var i; var ret = []; for (i = 0; i < a.length; i+=2) { if (a[i] === b[i]) { if (a[i+1] <= b[i+1]) continue; ret.push(b[i+1], a[i+1]); } else { if (a[i] > b[i]) continue; ret.push(a[i], b[i]); } } return ret; } TK.MeterBase = TK.class({ /** * TK.MeterBase is a base class to build different meters such as {@link TK.LevelMeter} from. * TK.MeterBase uses {@link TK.Gradient} and contains a {@link TK.Scale} widget. * TK.MeterBase inherits all options from {@link TK.Scale}. * * Note that the two options format_labels and * scale_base have different names here. * * Note that level meters with high update frequencies can be very demanding when it comes * to rendering performance. These performance requirements can be reduced by increasing the * segment size using the segment option. Using a segment, the different level * meter positions are reduced. This widget will take advantage of that by avoiding rendering those * changes to the meter level, which fall into the same segment. * * @class TK.MeterBase * * @extends TK.Widget * * @param {Object} [options={ }] - An object containing initial options. * * @property {String} [options.layout="left"] - A string describing the layout of the meter. * Possible values are "left", "right", "top" and * "bottom". "left" and "right" are vertical * layouts, where the meter is on the left or right of the scale, respectively. Similarly, * "top" and "bottom" are horizontal layouts in which the meter * is at the top or the bottom, respectively. * @property {Integer} [options.segment=1] - Segment size. Pixel positions of the meter level are * rounded to multiples of this size. This can be used to give the level meter a LED effect and to * reduce processor load. * @property {Number} [options.value=0] - Level value. * @property {Number} [options.base=false] - The base value of the meter. If set to false, * the base will coincide with the minimum value options.min. The meter level is drawn * starting from the base to the value. * @property {Number} [options.label=0] - Value to be displayed on the label. * @property {Function} [options.format_label=TK.FORMAT("%.2f")] - Function for formatting the * label. * @property {Boolean} [options.show_label=false] - If set to true a label is displayed. * @property {Number} [options.title=false] - The title of the TK.MeterBase. Set to `false` to hide it. * @property {Boolean} [options.show_scale=true] - Set to false to hide the scale. * @property {Number|Boolean} [options.scale_base=false] - Base of the meter scale, see {@link TK.Scale} for more information. * @property {Boolean} [options.show_labels=true] - If true, display labels on the * scale. * @property {Function} [options.format_labels=TK.FORMAT("%.2f")] - Function for formatting the * scale labels. This is passed to TK.Scale as option labels. * */ _class: "MeterBase", Extends: TK.Widget, Implements: [TK.Gradient], _options: Object.assign(Object.create(TK.Widget.prototype._options), TK.Gradient.prototype._options, TK.Scale.prototype._options, { layout: "string", segment: "number", value: "number", base: "number|boolean", min: "number", max: "number", label: "number", title: "string|boolean", show_labels: "boolean", format_label: "function", scale_base: "number|boolean", format_labels: "function", background: "string|boolean", gradient: "object|boolean" }), options: { layout: "left", segment: 1, value: 0, base: false, label: 0, title: false, show_labels: true, format_label: TK.FORMAT("%.2f"), levels: [1, 5, 10], // array of steps where to draw labels scale_base: false, format_labels: TK.FORMAT("%.2f"), background: false, gradient: false }, static_events: { set_label: function(value) { /** * Is fired when the label changed. * The argument is the actual label value. * * @event TK.MeterBase#labelchanged * * @param {string} label - The label of the {@link TK.MeterBase}. */ this.fire_event("labelchanged", value); }, set_title: function(value) { /** * Is fired when the title changed. * The argument is the actual title. * * @event TK.MeterBase#titlechanged * * @param {string} title - The title of the {@link TK.MeterBase}. */ this.fire_event("titlechanged", value); }, set_segment: function(value) { // what is this supposed to do? // -> probably invalidate the value to force a redraw this.set("value", this.options.value); }, set_value: function(value) { /** * Is fired when the value changed. * The argument is the actual value. * * @event TK.MeterBase#valuechanged * * @param {number} value - The value of the {@link TK.MeterBase}. */ this.fire_event("valuechanged", value); }, set_base: function(value) { if (value === false) { var O = this.options; O.base = value = O.min; } /** * Is fired when the base value changed. * The argument is the actual base value. * * @event TK.MeterBase#basechanged * * @param {number} base - The value of the base. */ this.fire_event("basechanged", value); }, set_layout: function () { var O = this.options; this.set("value", O.value); this.set("min", O.min); this.trigger_resize(); }, rangedchanged: function() { var gradient = this.options.gradient; if (gradient) { this.set("gradient", gradient); } }, }, initialize: function (options) { var E; TK.Widget.prototype.initialize.call(this, options); var O = this.options; /** * @member {HTMLDivElement} TK.MeterBase#element - The main DIV container. * Has class toolkit-meter-base. */ if (!(E = this.element)) this.element = E = TK.element("div"); TK.add_class(E, "toolkit-meter-base"); this.widgetize(E, false, true, true); this._bar = TK.element("div", "toolkit-bar"); /** * @member {HTMLCanvas} TK.MeterBase#_canvas - The canvas element drawing the mask. * Has class toolkit-mask. */ this._canvas = document.createElement("canvas"); TK.add_class(this._canvas, "toolkit-mask"); this._fillstyle = false; E.appendChild(this._bar); this._bar.appendChild(this._canvas); /** * @member {HTMLDivElement} TK.MeterBase#_bar - The DIV element containing the masks * and drawing the background. Has class toolkit-bar. */ this.delegate(this._bar); this._last_meters = []; this._current_meters = []; this.set("label", O.value); this.set("base", O.base); }, destroy: function () { this._bar.remove(); TK.Widget.prototype.destroy.call(this); }, redraw: function () { var I = this.invalid; var O = this.options; var E = this.element; if (this._fillstyle === false) { this._canvas.style.removeProperty("background-color"); TK.S.add(function() { this._fillstyle = TK.get_style(this._canvas, "background-color"); TK.S.add(function() { this._canvas.getContext("2d").fillStyle = this._fillstyle; this._canvas.style.setProperty("background-color", "transparent", "important"); this.trigger_draw(); }.bind(this), 3); }.bind(this), 2); } if (I.reverse) { I.reverse = false; TK.toggle_class(E, "toolkit-reverse", O.reverse); } if (I.gradient || I.background) { I.gradient = I.background = false; this.draw_gradient(this._bar, O.gradient, O.background); } TK.Widget.prototype.redraw.call(this); if (I.layout) { I.layout = false; TK.remove_class(E, "toolkit-vertical", "toolkit-horizontal", "toolkit-left", "toolkit-right", "toolkit-top", "toolkit-bottom"); var scale = this.scale ? this.scale.element : null; var bar = this._bar; switch (O.layout) { case "left": TK.add_class(E, "toolkit-vertical", "toolkit-left"); if (scale) TK.insert_after(scale, bar); break; case "right": TK.add_class(E, "toolkit-vertical", "toolkit-right"); if (scale) TK.insert_after(bar, scale); break; case "top": TK.add_class(E, "toolkit-horizontal", "toolkit-top"); if (scale) TK.insert_after(scale, bar); break; case "bottom": TK.add_class(E, "toolkit-horizontal", "toolkit-bottom"); if (scale) TK.insert_after(bar, scale); break; default: throw("unsupported layout"); } } if (this._fillstyle === false) return; if (I.basis && O._height > 0 && O._width > 0) { this._canvas.setAttribute("height", Math.round(O._height)); this._canvas.setAttribute("width", Math.round(O._width)); /* FIXME: I am not sure why this is even necessary */ this._canvas.style.width = O._width + "px"; this._canvas.style.height = O._height + "px"; this._canvas.getContext("2d").fillStyle = this._fillstyle; } if (I.value && O.show_label) { this.label.set("label", O.format_label(O.value)); } if (I.value || I.basis || I.min || I.max) { I.basis = I.value = I.min = I.max = false; this.draw_meter(); } }, resize: function() { var O = this.options; TK.Widget.prototype.resize.call(this); var w = TK.inner_width(this._bar); var h = TK.inner_height(this._bar); this.set("_width", w); this.set("_height", h); var i = vert(O) ? h : w; this.set("basis", i); this._last_meters.length = 0; this._fillstyle = false; }, calculate_meter: function(to, value, i) { var O = this.options; // Set the mask elements according to options.value to show a value in // the meter bar var base = O.base; var segment = O.segment|0; var reverse = !!O.reverse; var size = O.basis|0; /* At this point the whole meter bar is filled. We now want * to clear the area between base and value. */ /* canvas coordinates are reversed */ var v1 = this.val2px(base)|0; var v2 = this.val2px(value)|0; if (segment !== 1) v2 = segment*(Math.round(v2/segment)|0); if (v2 < v1) { to[i++] = v2; to[i++] = v1; } else { to[i++] = v1; to[i++] = v2; } return i; }, draw_meter: function () { var O = this.options; var w = Math.round(O._width); var h = Math.round(O._height); var i, j; if (!(w > 0 && h > 0)) return; var a = this._current_meters; var tmp = this._last_meters; var i = this.calculate_meter(a, O.value, 0); if (i < a.length) a.length = i; make_interval(a); this._last_meters = a; this._current_meters = tmp; var diff; if (tmp.length === a.length) { diff = cmp_intervals(tmp, a)|0; } else diff = 4; if (!diff) return; // FIXME: this is currently broken for some reason if (diff == 1) diff = 4; var ctx = this._canvas.getContext("2d"); ctx.fillStyle = this._fillstyle; var is_vertical = vert(O); if (diff === 1) { /* a - tmp is non-empty */ clear_interval(ctx, w, h, subtract_intervals(a, tmp), is_vertical); return; } if (diff === 2) { /* tmp - a is non-empty */ fill_interval(ctx, w, h, subtract_intervals(tmp, a), is_vertical); return; } draw_full(ctx, w, h, a, is_vertical); }, // HELPERS & STUFF _val2seg: function (val) { // rounds values to fit in the segments size // always returns values without taking options.reverse into account var s = +this.val2px(this.snap(val)); s -= s % +this.options.segment; if (this.options.reverse) s = +this.options.basis - s; return s; }, has_base: function() { var O = this.options; return O.base > O.min; }, }); /** * @member {TK.Scale} TK.MeterBase#scale - The {@link TK.Scale} of the meter. */ TK.ChildWidget(TK.MeterBase, "scale", { create: TK.Scale, map_options: { format_labels: "labels", scale_base: "base", }, inherit_options: true, show: true, toggle_class: true, static_events: { set: function(key, value) { var p = this.parent; if (p) p.fire_event("scalechanged", key, value); }, }, }); /** * @member {TK.Label} TK.MeterBase#title - The {@link TK.Label} displaying the title. * Has class toolkit-title. */ TK.ChildWidget(TK.MeterBase, "title", { create: TK.Label, show: false, option: "title", default_options: { "class" : "toolkit-title" }, map_options: { "title" : "label" }, toggle_class: true, }); /** * @member {TK.Label} TK.MeterBase#label - The {@link TK.Label} displaying the label. */ TK.ChildWidget(TK.MeterBase, "label", { create: TK.Label, show: false, default_options: { "class" : "toolkit-value" }, toggle_class: true, }); })(this, this.TK);